Compare commits
286 Commits
536d520744
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d3c3e5a74f | |||
| 92834559bc | |||
| 3938170a16 | |||
| 1c5b45732f | |||
| 00c9eddd19 | |||
| 6aaf292ad9 | |||
| a16ba169fd | |||
| 9b583ccc91 | |||
| 5520434d53 | |||
| bd041136c0 | |||
| 6859846b7c | |||
| d976cd03a3 | |||
| 41bff9dff6 | |||
| 65d8605b6c | |||
| d50120d1d2 | |||
| 0ec0aaf174 | |||
| 45d21026b7 | |||
| 71f0f53e6a | |||
| 113718b553 | |||
| da26d648a9 | |||
| d770b07609 | |||
| 73278667e0 | |||
| 76682e3be0 | |||
| 629d8b3e26 | |||
| 37720c4705 | |||
| 4f260f9257 | |||
| c8efbc2a90 | |||
| 2cb0b57281 | |||
| 80d6aad54b | |||
| 58843a1ef4 | |||
| 7add9384c1 | |||
| 6720ca3474 | |||
| 2e797f86b7 | |||
| 5ea5c3a411 | |||
| 3195787461 | |||
| 97c47a120f | |||
| 028dc2c8b6 | |||
| 51b6d0cdff | |||
| e5929694c3 | |||
| b768da3ab8 | |||
| 45ca57bf3f | |||
| 14bbfc4ab7 | |||
| 770469c4aa | |||
| b4436143a6 | |||
| 7c99298419 | |||
| 461ac81221 | |||
| 1c4c5af0e4 | |||
| d00294c86d | |||
| 82426b1941 | |||
| ebab836554 | |||
| ccc5419369 | |||
| d8ec4f1af4 | |||
| 9c9a51263c | |||
| 1a79c0fbfb | |||
| 86a45612c8 | |||
| 05b0fde197 | |||
| c91cb1cd9e | |||
| 3faa7a4f1c | |||
| b5419cd2c0 | |||
| 5c508ed14c | |||
| 825d0297c4 | |||
| 4bbc47faab | |||
| ba7c790f38 | |||
| adee25deb6 | |||
| 4acbf04322 | |||
| feb776616f | |||
| 72042598b0 | |||
| e95e522d4d | |||
| 40cba6a567 | |||
| 821cf12ade | |||
| deafd46a0e | |||
| dbb6bc4a55 | |||
| e833b923c8 | |||
| 2b0413c43e | |||
| a51a5c5341 | |||
| 80aaf855a3 | |||
| e42612e75f | |||
| 7a0001d8f4 | |||
| 3598d38b8e | |||
| d5ecb54d6e | |||
| ac98dc496c | |||
| 49b471eff9 | |||
| 647ebcb673 | |||
| 4c38bc05d3 | |||
| 907607e30b | |||
| bdbfef7110 | |||
| 8092000880 | |||
| e57d689fb2 | |||
| 75efd506f5 | |||
| 32a1885f0d | |||
| 7bb85aac41 | |||
| f37047f746 | |||
| 6b8965aaf6 | |||
| 6140d68875 | |||
| b46922fe92 | |||
| f6e8b29640 | |||
| e0133b9098 | |||
| 724375c884 | |||
| 989b17902b | |||
| 441aa0b9d2 | |||
| 540712500f | |||
| 17abd62261 | |||
| 3a4b2ff3eb | |||
| 2877fd90a4 | |||
| e6abd4ae73 | |||
| 29b053b3c0 | |||
| f11ad35708 | |||
| 84b388fae2 | |||
| 0a5111a959 | |||
| 31846c4212 | |||
| 2e8daad802 | |||
| a94d340ec0 | |||
| 8356425814 | |||
| 2a6f526bc9 | |||
| 996403ad86 | |||
| 4e117c04ea | |||
| 79105dcec8 | |||
| d3c0e57a08 | |||
| af912662de | |||
| c69e45a571 | |||
| 130cc840c7 | |||
| 704764bd7e | |||
| 426bafd169 | |||
| dc4593f8b5 | |||
| 341b130d9b | |||
| af47b24450 | |||
| aeca2fd7cb | |||
| 960598f504 | |||
| 6f998048b6 | |||
| 9b9a857720 | |||
| f127f07ab2 | |||
| dabfc128b3 | |||
| dd870859b5 | |||
| 700b3b4301 | |||
| cc34b9c3cc | |||
| 53b700f929 | |||
| fc4b46566e | |||
| 7209f53af9 | |||
| 283041fe2f | |||
| 85b0c3a980 | |||
| cbbcdae66d | |||
| c5e54da8de | |||
| a82d69c3e5 | |||
| fc837bfd43 | |||
| 7a9dc3d3fc | |||
| 1fefa4b6cb | |||
| 6a6f3f7614 | |||
| fe8c11b706 | |||
| b287707359 | |||
| 64c8bd74b8 | |||
| 1c0d7e9eb6 | |||
| a842af2405 | |||
| 81004cf0a5 | |||
| 3ba78cd10a | |||
| 213aad97a4 | |||
| c366ed5adc | |||
| 664efb9ed5 | |||
| adcd8010a5 | |||
| 863e49e518 | |||
| 56139a42fe | |||
| e3efbd4552 | |||
| 4d82ed4031 | |||
| b62f1039db | |||
| 9f78207aa2 | |||
| 3864d120fb | |||
| 0f6f91e61a | |||
| 091fb97291 | |||
| abeab9d3e4 | |||
| 972456a246 | |||
| 69068e2e39 | |||
| a401ea5a4d | |||
| 632891e6e5 | |||
| ff143c286d | |||
| 85ecd1f54f | |||
| 05c7aeb702 | |||
| d0526c06b7 | |||
| cb49fd9d6d | |||
| fe8b24ca0b | |||
| 6612c76dc6 | |||
| 86bc737f4c | |||
| 27da842a20 | |||
| a5b1d34cec | |||
| 1e83bb34bd | |||
| 8c86b50d09 | |||
| 8312a6cf81 | |||
| 1d1fb6285b | |||
| 835ef2c8ea | |||
| 09d4613e46 | |||
| 22d5d21c1f | |||
| 08a0157c95 | |||
| b8425cc059 | |||
| b220a5a762 | |||
| b011b60f9c | |||
| 442e005d27 | |||
| 908ae938b5 | |||
| b12fb4770b | |||
| 6150a39889 | |||
| 06af90d35e | |||
| 9723e0f528 | |||
| 118ec9f6b0 | |||
| 3d76c4a447 | |||
| 0b2d7ac50d | |||
| 3f995a74bb | |||
| df95a2429f | |||
| 5f63fb0a3f | |||
| c435413563 | |||
| c8b0663508 | |||
| fb6a4be797 | |||
| dfb0ea2d8f | |||
| f12a0b6a36 | |||
| 5ed3c28491 | |||
| 77cd9a5205 | |||
| 814262bd83 | |||
| 2b016f91af | |||
| 217c5d9935 | |||
| a225b655d9 | |||
| bdb3b3bc34 | |||
| e5a3f2524e | |||
| 9337f37da8 | |||
| 7ea94a60ab | |||
| 05006cb5df | |||
| 69087988b1 | |||
| a14fbb3df3 | |||
| c50f65e8ea | |||
| ea2507dbc0 | |||
| f60a5ea5bc | |||
| 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 |
+15
-1
@@ -2,8 +2,22 @@
|
||||
"mcpServers": {
|
||||
"mysql-mcp": {
|
||||
"command": "bash",
|
||||
"args": ["scripts/linux_mcp_mysql.sh"],
|
||||
"args": [
|
||||
"scripts/linux_mcp_mysql.sh"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"gitea-mcp": {
|
||||
"command": "bash",
|
||||
"args": [
|
||||
"scripts/mcp_gitea.sh"
|
||||
],
|
||||
"env": {
|
||||
"GITEA_HOST": "https://axs.kuma1xn.vip",
|
||||
"GITEA_ACCESS_TOKEN_FILE": "/mnt/c/Users/uzuma/.codex/secrets/gitea-axs.token",
|
||||
"HTTPS_PROXY": "socks5://127.0.0.1:10808",
|
||||
"HTTP_PROXY": "socks5://127.0.0.1:10808"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
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`
|
||||
@@ -11,7 +11,7 @@ alwaysApply: false
|
||||
| 调用方 | 机制 |
|
||||
|--------|------|
|
||||
| 管理端 API | JWT Bearer + 可选 TOTP |
|
||||
| Agent | `X-API-Key`(全局或 per-server `agent_api_key`) |
|
||||
| Agent | `X-API-Key`(仅 per-server `agent_api_key`;经 `POST /api/servers/{id}/agent-key` 生成,BL-06 起无 global 回退) |
|
||||
| WebSocket | JWT query param |
|
||||
| 安装向导 | 无 JWT(仅安装模式可用) |
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"plugins": {
|
||||
"compound-engineering": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-2
@@ -5,13 +5,14 @@
|
||||
|
||||
## 栈
|
||||
|
||||
FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA(14 页)· Vite → `web/app/`
|
||||
FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA(15 页)· Vite → `web/app/`
|
||||
|
||||
## 环境(勿在仓库写密码)
|
||||
|
||||
- 生产: `https://api.synaglobal.vip` · 端口 8600 · `ssh nexus`
|
||||
- 本地: `~/Nexus` 或 `$NEXUS_ROOT` · 见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)
|
||||
- 凭据: 仅 `.env`(不入 git)
|
||||
- **Gitea**:clone/pull 可匿名;**push** 用 `bash scripts/git-push.sh`(凭据在 `deploy/nexus-1panel.secrets.sh`,已 gitignore,勿提交)
|
||||
|
||||
## 关键路径
|
||||
|
||||
@@ -28,13 +29,25 @@ FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA(14 页)· Vite
|
||||
|
||||
见 `.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 deploy/pre_deploy_check.sh && bash deploy/deploy-production.sh
|
||||
```
|
||||
|
||||
前端: `cd frontend && npx vite build` → tar/scp(见 `deploy/deploy-frontend.sh`)
|
||||
仅同步源码(不重建镜像):`bash deploy/rsync-local-to-server.sh`
|
||||
仍走 Gitea 拉取:`NEXUS_DEPLOY_VIA_GIT=1 bash deploy/deploy-production.sh`
|
||||
|
||||
前端: `cd frontend && npx vite build` → 部署脚本内 tar/scp 到 `web/app/`
|
||||
|
||||
## 进度条(改代码时输出)
|
||||
|
||||
|
||||
@@ -27,3 +27,16 @@ standards/
|
||||
web/uploads
|
||||
backups/
|
||||
deploy/gate_log.jsonl
|
||||
|
||||
# Release/build local artifacts
|
||||
frontend-v2/node_modules
|
||||
.venv
|
||||
.venv-*
|
||||
venv
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
.playwright-mcp
|
||||
2025.2
|
||||
=2025.2
|
||||
*.bak-
|
||||
tmp/
|
||||
|
||||
@@ -35,18 +35,58 @@ jobs:
|
||||
run: mypy server/ --ignore-missing-imports
|
||||
continue-on-error: true # Type errors don't block deployment yet
|
||||
|
||||
- name: Run tests with coverage
|
||||
- name: Run fast tests with coverage
|
||||
run: |
|
||||
coverage run -m pytest tests/ -v --tb=short
|
||||
coverage run -m pytest tests/ -m "not chain and not slow" -q --tb=short
|
||||
coverage report --fail-under=50
|
||||
coverage xml
|
||||
|
||||
- name: Run chain tests
|
||||
run: pytest tests/chain -m chain -q --tb=short
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
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 ──
|
||||
deploy:
|
||||
needs: test # Only deploy if tests pass
|
||||
|
||||
@@ -16,6 +16,7 @@ deploy/nexus-1panel.secrets.sh
|
||||
*.pem
|
||||
.env.*
|
||||
.venv/
|
||||
.venv-*/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
@@ -26,6 +27,7 @@ ENV/
|
||||
.megamemory/
|
||||
.playwright-mcp/
|
||||
frontend/test-results/
|
||||
frontend/e2e/.auth/
|
||||
tmp/
|
||||
|
||||
# IDE
|
||||
@@ -69,6 +71,7 @@ web/uploads/
|
||||
# Data (sensitive)
|
||||
web/data/config.php
|
||||
web/data/*.db
|
||||
web/data/1panel-hosts.json
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 项目概要
|
||||
|
||||
Nexus:2000+ 子机运维平台。后端 FastAPI + MySQL + Redis;前端 **Vue 3 + Vuetify 4 + TypeScript SPA**(14 页,`frontend/src/pages/`);构建输出 `web/app/`;安装向导 `web/app/install.html`。
|
||||
Nexus:2000+ 子机运维平台。后端 FastAPI + MySQL + Redis;前端 **Vue 3 + Vuetify 4 + TypeScript SPA**(15 页,`frontend/src/pages/`);构建输出 `web/app/`;安装向导 `web/app/install.html`。
|
||||
|
||||
## 本地开发
|
||||
|
||||
@@ -19,6 +19,13 @@ cd frontend && npm run dev # :3000/app/
|
||||
|
||||
路径见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)。
|
||||
|
||||
## Git(本机 + Gitea)
|
||||
|
||||
- 改完代码:**`git commit` 后 `bash scripts/git-push.sh` 推到 Gitea**(凭据 `deploy/nexus-1panel.secrets.sh`);大仓库可用 `scripts/git-push-axs-full.sh`。
|
||||
- 本地 commit 仍用于回滚;**不要漏推 Gitea**(用户约定 2026-07-09)。
|
||||
- `.env`、`SECRETS.md`、`*.pem`、`deploy/nexus-1panel.secrets.sh` 已在 `.gitignore`,**勿提交**。
|
||||
- 改代码仍写 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)。
|
||||
|
||||
## 强制约束
|
||||
|
||||
- 完美实现:`.cursor/rules/perfect-implementation.mdc`
|
||||
@@ -32,13 +39,58 @@ cd frontend && npm run dev # :3000/app/
|
||||
|
||||
## 部署
|
||||
|
||||
默认 **本机 rsync → SSH 生产机**;**代码先 push Gitea 再部署**(见上节 Git):
|
||||
|
||||
```bash
|
||||
bash deploy/pre_deploy_check.sh
|
||||
bash deploy/deploy-production.sh # 需 SSH nexus + 用户批准
|
||||
bash scripts/git-push.sh # 或 git-push-axs-full.sh
|
||||
bash deploy/deploy-production.sh # rsync + Docker upgrade --skip-git + 前端
|
||||
```
|
||||
|
||||
可选:`NEXUS_DEPLOY_VIA_GIT=1` 恢复远程 `git pull`。
|
||||
|
||||
详情见功能指南 §17–18。
|
||||
|
||||
## 归档文档
|
||||
|
||||
历史审计合并卷:`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 个文件 → 停止**。
|
||||
|
||||
+5
-1
@@ -2,7 +2,11 @@
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
openssh-client \
|
||||
rsync \
|
||||
sshpass \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
+21
-2
@@ -12,11 +12,25 @@ RUN npm ci --ignore-scripts
|
||||
COPY frontend/ ./
|
||||
RUN npx vite build
|
||||
|
||||
# ── Stage 1b: React/Vite App V2 ──
|
||||
FROM node:20-bookworm-slim AS frontend_v2
|
||||
WORKDIR /build/frontend-v2
|
||||
|
||||
COPY frontend-v2/package.json frontend-v2/package-lock.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY frontend-v2/ ./
|
||||
RUN npm run build:safe
|
||||
|
||||
# ── Stage 2: Python API + built static assets ──
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
openssh-client \
|
||||
rsync \
|
||||
sshpass \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
@@ -26,9 +40,13 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server/ ./server/
|
||||
COPY web/agent/ ./web/agent/
|
||||
COPY --from=frontend_v2 /build/web/app-v2 ./web/app-v2/
|
||||
COPY --from=frontend /build/web/app ./web/app/
|
||||
# Install wizard (Alpine + Tailwind static page; not part of Vite build)
|
||||
# Login wallpaper cache (Vite does not emit this dir; sync also writes here at runtime)
|
||||
COPY web/app/wallpapers/ ./web/app/wallpapers/
|
||||
# Install wizard + static SPA assets not emitted by Vite
|
||||
COPY web/app/install.html ./web/app/install.html
|
||||
COPY web/app/servers_import_template.csv ./web/app/servers_import_template.csv
|
||||
RUN mkdir -p ./web/app/vendor
|
||||
COPY web/app/vendor/tailwindcss-browser.js \
|
||||
web/app/vendor/theme-init.js \
|
||||
@@ -38,6 +56,7 @@ COPY web/app/vendor/tailwindcss-browser.js \
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
RUN chmod +x ./docker/entrypoint.sh \
|
||||
&& test -f /app/web/app/install.html \
|
||||
&& test -f /app/web/app-v2/index.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 \
|
||||
|
||||
@@ -10,15 +10,19 @@
|
||||
| 安装向导 | `https://你的域名/app/install.html` |
|
||||
| 文档索引 | [docs/README.md](docs/README.md) |
|
||||
|
||||
生产 Docker 栈 **仅含 Nexus 容器**;MySQL / Redis 在宿主机或 1Panel 自行安装。
|
||||
|
||||
---
|
||||
|
||||
## 一键安装(公共仓库,推荐)
|
||||
## 一键命令速查(root · 公共仓库 · 无需令牌)
|
||||
|
||||
在 **root** 登录的 Linux 服务器上执行(仿 [1Panel](https://1panel.cn/) 风格,无需 Gitea Token):
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
```
|
||||
| 场景 | 一条命令 |
|
||||
|------|----------|
|
||||
| **仅装 Nexus**(1Panel 已就绪) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" \| bash` |
|
||||
| **重装服务器**(1Panel + Docker + Nexus) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" \| bash` |
|
||||
| **一键更新**(pull + 重建镜像) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/update.sh" \| bash` |
|
||||
| **强制无缓存重建**(entrypoint 变更后) | `curl -fsSL ".../deploy/update.sh" \| bash -s -- --no-cache` |
|
||||
| **已 clone** | `cd /opt/nexus && bash deploy/update.sh` |
|
||||
|
||||
指定资源档位(`1c4g` / `2c8g` 默认 / `4c16g`):
|
||||
|
||||
@@ -26,62 +30,41 @@ curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-in
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash -s -- --profile 4c16g
|
||||
```
|
||||
|
||||
可选:先下载再执行(便于审计脚本内容):
|
||||
**raw 地址**:`http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/<脚本名>`
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" -o /tmp/quick-install.sh
|
||||
bash /tmp/quick-install.sh --profile 2c8g
|
||||
```
|
||||
|
||||
**raw 地址说明**(匿名可访问,须返回 `#!/` 而非 HTML 404):
|
||||
|
||||
- `http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/<脚本名>`
|
||||
- 等价:`http://66.154.115.8:3000/raw/main/deploy/<脚本名>`
|
||||
|
||||
安装完成后在浏览器打开安装向导,完成 `.env` / MySQL / 管理员初始化。
|
||||
|
||||
**每台新服务器的密钥自动不同**:安装脚本生成 `MYSQL_*` 与 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY` 写入 `docker/.env.prod`,备份在 `/root/.nexus-install-secrets-*.env`。勿把生产密钥放进 `nexus-1panel.secrets.sh`(仅保留 `NEXUS_GITEA_TOKEN`)。
|
||||
安装完成后浏览器打开安装向导,完成 MySQL(自建)/ Redis(自建)/ 管理员初始化。
|
||||
|
||||
---
|
||||
|
||||
## 已 clone 到本机(跳过再次拉仓)
|
||||
## NX 统一运维菜单
|
||||
|
||||
装好后在服务器任意目录输入 **`nx`**(`install` / `update` / 首次运行 `nx` 会自动注册 `/usr/local/bin/nx`):
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
nx # 统一菜单:安装 + 一键更新 + 重启 + 日志 + 备份
|
||||
nx update # 非交互一键更新
|
||||
nx update --no-cache
|
||||
nx install-fresh # 全新安装
|
||||
nx health # 健康检查
|
||||
```
|
||||
|
||||
或交互菜单:
|
||||
| 菜单项 | 作用 |
|
||||
|--------|------|
|
||||
| `[4] 一键更新` | pull + 重建 Docker 镜像(等同 `deploy/update.sh`) |
|
||||
| `[5] 重启 Nexus` | 重启 Python 应用 |
|
||||
| `[6] 日志` | 跟踪容器日志 |
|
||||
| `[1][2]` | 安装 / 全新安装向导 |
|
||||
|
||||
```bash
|
||||
nx
|
||||
# 选「全新安装」或子命令: nx install-fresh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NX 运维菜单(装好后)
|
||||
|
||||
安装脚本会向 `PATH` 注册:
|
||||
全局命令(符号链接):
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `nx` | 交互主菜单(安装 / 升级 / 健康检查) |
|
||||
| `nx god` | **上帝菜单**:重启 Python、全栈重启、日志、升级、备份等 |
|
||||
| `nx` | 统一运维菜单 |
|
||||
| `nexus-update` | 等同 `deploy/update.sh` |
|
||||
| `nexus-fresh` | 等同 `install-nexus-fresh.sh` |
|
||||
| `nexus-install` | 快捷安装入口 |
|
||||
| `nexus-install` | 等同 `quick-install.sh` |
|
||||
|
||||
常用子命令:
|
||||
|
||||
```bash
|
||||
nx # 主菜单
|
||||
nx god # 上帝运维菜单
|
||||
nx install-fresh # 全新空库安装 → /app/install.html
|
||||
nx install-auto # 非交互安装(需已配置密钥,默认 2c8g)
|
||||
nx health # 健康检查 / 端口预检
|
||||
```
|
||||
|
||||
底层仍可直接调用:`bash deploy/nexus-1panel.sh install|upgrade|check|ports`
|
||||
> `nx god` 已合并进 `nx` 主菜单,仍可作为兼容别名使用。
|
||||
|
||||
---
|
||||
|
||||
@@ -89,30 +72,41 @@ nx health # 健康检查 / 端口预检
|
||||
|
||||
### 1Panel + Docker 生产(主路径)
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| [quick-install.sh](deploy/quick-install.sh) | **公共仓库 curl 入口**;clone + 调用全新安装 |
|
||||
| [install-nexus-fresh.sh](deploy/install-nexus-fresh.sh) | 全新安装(空库 → 安装向导);支持 `--skip-clone`、`--profile` |
|
||||
| [nexus-1panel.sh](deploy/nexus-1panel.sh) | 迁机 / 升级 / 检查 / 端口预检;可被 `nx` 调用 |
|
||||
| [nx](deploy/nx) | 交互主菜单 + 上帝菜单 |
|
||||
| [install-1panel-docker.sh](deploy/install-1panel-docker.sh) | 转调 `nx` 的 Docker 安装包装 |
|
||||
| [upgrade-1panel-docker.sh](deploy/upgrade-1panel-docker.sh) | 转调 `nx` 的升级包装 |
|
||||
| [download-install-fresh.sh](deploy/download-install-fresh.sh) | 从 Gitea API/raw 下载安装脚本(私有库需 Token) |
|
||||
| 脚本 | 用途 | curl 入口 |
|
||||
|------|------|-----------|
|
||||
| [quick-install.sh](deploy/quick-install.sh) | **公共仓库安装入口** | ✅ 上表 |
|
||||
| [install-1panel-docker.sh](deploy/install-1panel-docker.sh) | 1Panel + Docker + Nexus 全链 | ✅ 上表 |
|
||||
| [install-nexus-fresh.sh](deploy/install-nexus-fresh.sh) | 全新空库 → 安装向导 | `bash deploy/install-nexus-fresh.sh --skip-clone` |
|
||||
| [update.sh](deploy/update.sh) | **一键更新**(pull + 重建) | ✅ 上表 |
|
||||
| [upgrade-1panel-docker.sh](deploy/upgrade-1panel-docker.sh) | 转调 `nexus-1panel.sh upgrade` | — |
|
||||
| [nexus-1panel.sh](deploy/nexus-1panel.sh) | 安装 / 升级 / check / ports 底层 | — |
|
||||
| [nx](deploy/nx) | **统一交互菜单** | `nx` |
|
||||
| [uninstall-mysql-compose.sh](deploy/uninstall-mysql-compose.sh) | 卸载遗留 Compose MySQL 容器 | — |
|
||||
| [uninstall-redis-compose.sh](deploy/uninstall-redis-compose.sh) | 卸载遗留 Compose Redis 容器 | — |
|
||||
| [sync-install-wizard-to-container.sh](deploy/sync-install-wizard-to-container.sh) | 热同步 install.html 到容器 | — |
|
||||
| [download-install-fresh.sh](deploy/download-install-fresh.sh) | 下载安装脚本(私有库可选 Token) | — |
|
||||
|
||||
详细说明:[deploy/README-1panel.md](deploy/README-1panel.md)
|
||||
|
||||
### 更新选项
|
||||
|
||||
```bash
|
||||
bash deploy/update.sh # 默认:Git 拉取(若有)+ 重建容器
|
||||
bash deploy/update.sh --check # 仅查 Git
|
||||
bash deploy/update.sh --no-cache # 无缓存重建镜像
|
||||
bash deploy/update.sh --no-backup # 跳过 MySQL 备份
|
||||
```
|
||||
|
||||
### Docker Compose 与档位
|
||||
|
||||
| 路径 | 用途 |
|
||||
|------|------|
|
||||
| [docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) | 生产 Compose(变量化 CPU/内存) |
|
||||
| [docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) | 生产 Compose(**仅 nexus 服务**) |
|
||||
| [docker/profiles/1c4g.env](docker/profiles/1c4g.env) | 1 核 4G |
|
||||
| [docker/profiles/2c8g.env](docker/profiles/2c8g.env) | 2 核 8G(默认) |
|
||||
| [docker/profiles/4c16g.env](docker/profiles/4c16g.env) | 4 核 16G |
|
||||
| [docker/README.md](docker/README.md) | 镜像与本地开发说明 |
|
||||
|
||||
安装参数:`--profile 1c4g|2c8g|4c16g`
|
||||
|
||||
### 经典 SSH / Supervisor 部署(宝塔 / 裸机)
|
||||
|
||||
| 脚本 | 用途 |
|
||||
@@ -122,20 +116,10 @@ nx health # 健康检查 / 端口预检
|
||||
| [deploy-on-server.sh](deploy/deploy-on-server.sh) | 服务器侧部署 |
|
||||
| [deploy-production.sh](deploy/deploy-production.sh) | 生产发布流程 |
|
||||
| [deploy-frontend.sh](deploy/deploy-frontend.sh) | 构建 Vue 前端并打包到 `web/app/` |
|
||||
| [pre_deploy_check.sh](deploy/pre_deploy_check.sh) | **7 道门控**(changelog / 审计 / 测试 / ruff / import / bandit / review) |
|
||||
| [health_monitor.sh](deploy/health_monitor.sh) | cron 健康检查,`/health` 失败重启 |
|
||||
| [db_backup.sh](deploy/db_backup.sh) | MySQL 备份(30 天保留) |
|
||||
| [run_test_api_on_server.sh](deploy/run_test_api_on_server.sh) | 服务器上跑 `test_api` |
|
||||
| [pre_deploy_check.sh](deploy/pre_deploy_check.sh) | **7 道门控** |
|
||||
| [health_monitor.sh](deploy/health_monitor.sh) | cron 健康检查 |
|
||||
| [db_backup.sh](deploy/db_backup.sh) | MySQL 备份 |
|
||||
| [uninstall.sh](deploy/uninstall.sh) | 卸载 |
|
||||
| [check_shell_eol.sh](deploy/check_shell_eol.sh) | Shell 脚本 EOL 检查 |
|
||||
|
||||
### 本地与验收
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| [scripts/local_verify.sh](scripts/local_verify.sh) | L2b 本地验证(ruff / import / 前端 typecheck 等) |
|
||||
| [scripts/run_nexus_acceptance.py](scripts/run_nexus_acceptance.py) | L4 验收 |
|
||||
| [verify.py](verify.py) | 快速验证入口 |
|
||||
|
||||
---
|
||||
|
||||
@@ -143,10 +127,12 @@ nx health # 健康检查 / 端口预检
|
||||
|
||||
| 场景 | 做法 |
|
||||
|------|------|
|
||||
| **新 VPS、空 MySQL** | `curl \| bash` 或 `install-nexus-fresh.sh` → 浏览器走完安装向导 |
|
||||
| **已有数据、换机** | `nexus-1panel.sh install`(带备份/恢复逻辑)或 `nx` 迁机流程 |
|
||||
| **仅升级代码/镜像** | `nx` → 升级,或 `upgrade-1panel-docker.sh` / `nexus-1panel.sh upgrade` |
|
||||
| **不必重装整台 VPS** | 在 `/opt/nexus` 内 `git pull` + `nx god` 重启即可 |
|
||||
| **新 VPS、空库** | `curl \| quick-install.sh` → 1Panel 自建 MySQL/Redis → `/app/install.html` |
|
||||
| **已有数据、换机** | `nexus-1panel.sh install --from-env` 或 `nx` → 安装向导 |
|
||||
| **仅升级代码/镜像** | `nx update` 或 `bash deploy/update.sh` |
|
||||
| **不必重装 VPS** | `cd /opt/nexus && nx update` |
|
||||
|
||||
每台新服务器自动生成唯一 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`(写入 `docker/.env.prod`,备份 `/root/.nexus-install-secrets-*.env`)。MySQL 密码由自建库时自行设定。
|
||||
|
||||
---
|
||||
|
||||
@@ -155,34 +141,27 @@ nx health # 健康检查 / 端口预检
|
||||
```
|
||||
Nexus/
|
||||
├── server/ # FastAPI 后端(Clean Architecture)
|
||||
├── frontend/ # Vue 3 + Vuetify 4 SPA → 构建到 web/app/
|
||||
├── frontend/ # Vue 3 + Vuetify 4 SPA → 构建到 web/app/
|
||||
├── web/app/ # 静态资源挂载点(构建产物 gitignore)
|
||||
├── deploy/ # 安装、门控、健康检查、运维脚本
|
||||
├── deploy/ # 安装、更新、门控、nx 菜单
|
||||
├── docker/ # Compose、镜像、profiles
|
||||
├── docs/ # 设计 / changelog / 审计 / 报告
|
||||
├── tests/ # API 与集成测试
|
||||
└── standards/ # 开发与逐行审计标准
|
||||
```
|
||||
|
||||
启动模式:
|
||||
|
||||
- **无 `.env`**:仅 `/api/install/` + 安装页(安装模式)
|
||||
- **有 `.env`**:完整业务 API + 后台任务
|
||||
|
||||
---
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 后端(需 .env)
|
||||
pip install -r requirements.txt
|
||||
uvicorn server.main:app --reload --port 8600
|
||||
|
||||
# 前端
|
||||
cd frontend && npm run dev # http://localhost:3000/app/
|
||||
```
|
||||
|
||||
Linux 路径与本地联调:[docs/project/linux-dev-paths.md](docs/project/linux-dev-paths.md) · [UBUNTU-DEV-README.txt](UBUNTU-DEV-README.txt)
|
||||
Linux 路径:[docs/project/linux-dev-paths.md](docs/project/linux-dev-paths.md) · [UBUNTU-DEV-README.txt](UBUNTU-DEV-README.txt)
|
||||
|
||||
---
|
||||
|
||||
@@ -190,12 +169,11 @@ Linux 路径与本地联调:[docs/project/linux-dev-paths.md](docs/project/lin
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [docs/README.md](docs/README.md) | 文档总索引(SSOT) |
|
||||
| [docs/README.md](docs/README.md) | 文档总索引 |
|
||||
| [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md) | 功能 / API / 14 页 |
|
||||
| [docs/project/AI-HANDOFF-2026-06-03.md](docs/project/AI-HANDOFF-2026-06-03.md) | 当前会话接续 |
|
||||
| [AGENTS.md](AGENTS.md) · [CLAUDE.md](CLAUDE.md) | Agent 薄入口 |
|
||||
|
||||
变更记录:`docs/changelog/` · 门控审计:`docs/audit/`
|
||||
变更:`docs/changelog/` · 门控:`docs/audit/`
|
||||
|
||||
---
|
||||
|
||||
@@ -203,10 +181,9 @@ Linux 路径与本地联调:[docs/project/linux-dev-paths.md](docs/project/lin
|
||||
|
||||
| 用途 | 地址 |
|
||||
|------|------|
|
||||
| 仓库首页(产品说明 / README) | http://66.154.115.8:3000/admin/Nexus |
|
||||
| Git 克隆 / push | http://66.154.115.8:3000/admin/Nexus.git |
|
||||
| 仓库首页 | http://66.154.115.8:3000/admin/Nexus |
|
||||
| Git | http://66.154.115.8:3000/admin/Nexus.git |
|
||||
|
||||
- 分支:`main`
|
||||
- 提交前请通过 `deploy/pre_deploy_check.sh`(7 道门控)
|
||||
分支 `main` · 提交前跑 `deploy/pre_deploy_check.sh`(7 道门控)
|
||||
|
||||
**注意**:`deploy/nexus-1panel.secrets.sh`、根目录 `.env`、`SECRETS.md` 含敏感信息,**勿提交到 Git**。生产密钥仅保存在服务器与本机私密文档中。
|
||||
**勿提交**:`deploy/nexus-1panel.secrets.sh`、根目录 `.env`、`SECRETS.md`
|
||||
|
||||
@@ -12,7 +12,8 @@ upstream nexus_api {
|
||||
# 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';
|
||||
|
||||
client_max_body_size 100m;
|
||||
client_max_body_size 500m;
|
||||
client_body_timeout 600s;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://nexus_api;
|
||||
@@ -21,8 +22,8 @@ location /api/ {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_connect_timeout 10s;
|
||||
}
|
||||
|
||||
|
||||
+213
-57
@@ -1,101 +1,257 @@
|
||||
# 1Panel + Docker 一键部署
|
||||
|
||||
## 重装服务器(推荐:1Panel + Docker + Nexus 一条链)
|
||||
> 完整知识文档:[docs/project/nexus-1panel-operations-knowledge.md](../docs/project/nexus-1panel-operations-knowledge.md)
|
||||
> 巡检归档(2026-06-06):[docs/project/nexus-god-menu-audit-summary.md](../docs/project/nexus-god-menu-audit-summary.md)
|
||||
|
||||
## 脚本速查(公共仓库 · 无需令牌)
|
||||
|
||||
| 脚本 | 用途 | 一条命令 |
|
||||
|------|------|----------|
|
||||
| `quick-install.sh` | 仅装 Nexus(1Panel 已就绪) | `curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" \| bash` |
|
||||
| `install-1panel-docker.sh` | 重装服务器全链 | `curl -fsSL ".../install-1panel-docker.sh" \| bash` |
|
||||
| `install-nexus-fresh.sh` | 全新空库安装 | `bash deploy/install-nexus-fresh.sh --skip-clone` |
|
||||
| **`update.sh`** | **一键更新** | `curl -fsSL ".../update.sh" \| bash` |
|
||||
| `nexus-1panel.sh` | 底层 install / upgrade | `bash deploy/nexus-1panel.sh upgrade` |
|
||||
| `fix-1panel-mysql-grant.sh` | 修复 1045 nexus@Docker | `bash deploy/fix-1panel-mysql-grant.sh` |
|
||||
| `test-1panel-redis.sh` | Redis 认证诊断 | `REDIS_PASS='...' bash deploy/test-1panel-redis.sh` |
|
||||
| `verify-1panel-install-wizard.sh` | 一键验收(网络/cron/镜像) | `bash deploy/verify-1panel-install-wizard.sh` |
|
||||
| `install_ops_cron.sh` | 宿主机 cron(巡检+备份) | `sudo bash deploy/install_ops_cron.sh` |
|
||||
| `mysql_dump_to_file.sh` | MySQL 备份(Docker exec) | 见 `db_backup.sh` / `nx` 菜单 `[9]` |
|
||||
| `upgrade.sh` | 自动识别 Docker/Supervisor | `bash deploy/upgrade.sh` |
|
||||
| **`nx`** | **统一运维菜单** | `nx` |
|
||||
|
||||
档位:`--profile 1c4g` · `2c8g`(默认)· `4c16g`
|
||||
|
||||
---
|
||||
|
||||
## 重装服务器(1Panel + Docker + Nexus)
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
```
|
||||
|
||||
脚本会:未装 1Panel 时提示跑官方安装 → 检测 Docker Compose → 拉代码并启动 Nexus 容器栈。
|
||||
|
||||
4 核 16G:
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash -s -- --profile 4c16g
|
||||
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g
|
||||
```
|
||||
|
||||
已装 1Panel,只装 Nexus:
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
```
|
||||
|
||||
## 仅 Nexus Docker(1Panel 已就绪)
|
||||
## 仅 Nexus Docker
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
```
|
||||
|
||||
安装完成后浏览器打开 `https://api.synaglobal.vip/app/install.html` 完成向导。
|
||||
安装完成后:`https://api.synaglobal.vip/app/install.html`
|
||||
|
||||
## Redis(自行安装,脚本不附带)
|
||||
---
|
||||
|
||||
Docker 栈仅含 **MySQL + Nexus**。Redis 请在宿主机或 1Panel 应用商店单独安装,监听 `127.0.0.1:6379`(或改 `docker/.env.prod` 的 `NEXUS_REDIS_URL`)。
|
||||
|
||||
安装向导步骤 3:Docker 部署时 Redis 主机填 **`host.docker.internal`**,端口 `6379`。
|
||||
|
||||
## 1Panel 反代(必须在面板 UI 操作)
|
||||
|
||||
**禁止** Agent/脚本直接 `mkdir` 或 `tee` 到 `/opt/1panel/apps/openresty/` — 会破坏 bind mount。
|
||||
|
||||
1. 1Panel → 应用商店 → 安装 **OpenResty**
|
||||
2. 网站 → 创建站点 → 反向代理 `http://127.0.0.1:8600`
|
||||
3. 自定义配置参考:`deploy/1panel/openresty-nexus.conf.example`
|
||||
|
||||
## 已 clone 到 /opt/nexus
|
||||
## 一键更新(日常)
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
cd /opt/nexus && bash deploy/update.sh
|
||||
# 或
|
||||
nx update
|
||||
# 或菜单
|
||||
nx # → [4] 一键更新
|
||||
```
|
||||
|
||||
## NX 运维菜单
|
||||
|
||||
```bash
|
||||
nx # 主菜单
|
||||
nx god # 上帝菜单(重启 Python / 升级 / 日志)
|
||||
```
|
||||
|
||||
装好后会注册:`nexus-install` · `nx` · `nexus-fresh`
|
||||
|
||||
## 脚本说明
|
||||
|
||||
| 文件 | 用途 |
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `install-1panel-docker.sh` | **重装服务器**一键链(1Panel + Docker + Nexus) |
|
||||
| `quick-install.sh` | 仅 Nexus Docker(1Panel 已装好) |
|
||||
| `install-nexus-fresh.sh` | 全新空库安装(被上面两个脚本调用) |
|
||||
| `nexus-1panel.sh` | 迁机 / 升级底层 |
|
||||
| `nx` | 交互菜单 |
|
||||
| `--check` | 仅查 Git,不重建 |
|
||||
| `--no-cache` | 无缓存重建镜像(entrypoint 变更后必用) |
|
||||
| `--no-backup` | 跳过 MySQL 备份 |
|
||||
| `--prune` | 清理悬空镜像 |
|
||||
|
||||
## 资源档位
|
||||
远程:
|
||||
|
||||
`--profile 1c4g` · `2c8g`(默认)· `4c16g`
|
||||
```bash
|
||||
curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
|
||||
curl -fsSL ".../update.sh" | bash -s -- --no-cache
|
||||
```
|
||||
|
||||
## 密钥(每台新服务器唯一)
|
||||
---
|
||||
|
||||
全新安装(`install-nexus-fresh.sh` / `--fresh`)会调用 `scripts/generate_nexus_secrets.py` 自动生成:
|
||||
## NX 全局命令
|
||||
|
||||
- `MYSQL_ROOT_PASSWORD` / `MYSQL_PASSWORD`
|
||||
- `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`(与安装向导算法一致)
|
||||
安装 / 升级 / 首次运行 `nx` 会自动注册到 `/usr/local/bin`:
|
||||
|
||||
写入 `docker/.env.prod`,备份到 `/root/.nexus-install-secrets-*.env`(chmod 600)。
|
||||
```bash
|
||||
# 手动注册(任意目录可用 nx)
|
||||
NEXUS_ROOT=/opt/nexus bash /opt/nexus/deploy/install-nx-cli.sh
|
||||
|
||||
nx # 统一运维菜单
|
||||
nexus-update # 等同 update.sh
|
||||
nexus-fresh # 等同 install-nexus-fresh.sh
|
||||
nexus-install # 等同 quick-install.sh
|
||||
```
|
||||
|
||||
| 菜单 / 子命令 | 功能 |
|
||||
|---------------|------|
|
||||
| `[1]` | 安装向导(档位 / 迁机 / skip-clone) |
|
||||
| `[2]` | 全新安装 → `/app/install.html` |
|
||||
| `[3]` | 状态 + `/health` + install.html 归档检查 |
|
||||
| `[4]` / `nx update` | **一键更新**(备份 + 重建镜像 + 升级后验收) |
|
||||
| `[5]` | 重启 Nexus 容器 |
|
||||
| `[6]` | 日志 |
|
||||
| `[7]` | 启动 / 停止 Compose 栈 |
|
||||
| `[8]` | 无缓存重建镜像 |
|
||||
| `[9]` | 立即备份 MySQL |
|
||||
| `[e]` / `nx cron` | 安装 health_monitor + db_backup crontab |
|
||||
| `[v]` / `nx verify` | 运行 `verify-1panel-install-wizard.sh` |
|
||||
|
||||
---
|
||||
|
||||
## 三层守护(1Panel Docker)
|
||||
|
||||
| 层 | 机制 | 说明 |
|
||||
|----|------|------|
|
||||
| Layer 1 | Compose `restart` + 容器 `healthcheck` | 容器崩溃自动拉起 |
|
||||
| Layer 2 | Python `self_monitor`(30s) | Redis/MySQL/WebSocket 自检 |
|
||||
| Layer 3 | 宿主机 `health_monitor.sh`(cron) | `/health` 连续失败 → `docker restart` + 可选 Telegram |
|
||||
|
||||
Layer 3 需**在宿主机**安装(容器内无法写 crontab):
|
||||
|
||||
```bash
|
||||
sudo nx cron
|
||||
# 或
|
||||
sudo bash /opt/nexus/deploy/install_ops_cron.sh
|
||||
```
|
||||
|
||||
MySQL 定时备份:每日 03:00 → `/var/backups/nexus/`(`docker exec` 进 1Panel MySQL 容器 dump,**无需**宿主机 `mysqldump`)。
|
||||
|
||||
---
|
||||
|
||||
## 安装完成后
|
||||
|
||||
- 向导锁定后 `install.html` → **`install.html.bak`**,外网 `/app/install.html` 返回 **404**(正常)
|
||||
- 登录:`https://你的域名/app/`
|
||||
- 验收:`sudo nx verify`
|
||||
|
||||
---
|
||||
|
||||
## MySQL / Redis(1Panel 应用商店)
|
||||
|
||||
Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安装。
|
||||
|
||||
### 1Panel 官方互联方式(必读)
|
||||
|
||||
[1Panel 维护者说明](https://github.com/1Panel-dev/1Panel/issues/11676):应用商店应用使用 **bridge + `1panel-network`**;**容器之间用容器名通信**(Docker 内置 DNS),不要用 `localhost` / `127.0.0.1` / `host.docker.internal`。
|
||||
|
||||
| 场景 | 正确主机 |
|
||||
|------|----------|
|
||||
| Nexus → 1Panel MySQL | `1Panel-mysql-xxxx`(容器列表中的名称) |
|
||||
| Nexus → 1Panel Redis | `1Panel-redis-xxxx` |
|
||||
| OpenResty → 应用 | `127.0.0.1:端口`(OpenResty 为 host 网络) |
|
||||
|
||||
`nx update` / `nexus-1panel.sh` 会:
|
||||
|
||||
1. 将 Nexus 加入 **`1panel-network`**(`docker/docker-compose.1panel.yml`)
|
||||
2. 自动探测 MySQL/Redis 容器名写入 `docker/.env.prod`(含 `NEXUS_REDIS_URL=redis://1Panel-redis-xxxx:6379/0`)
|
||||
3. 安装向导步骤 3 **预填 MySQL/Redis 容器名**
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **应用商店** → 安装 **MySQL**、**Redis**
|
||||
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
|
||||
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`)
|
||||
4. 填写 MySQL 密码;Redis:**用户名通常留空** + 应用参数密码(自动尝试 `:pass@` / `default`)
|
||||
|
||||
### 常见错误 1045(Access denied for 'nexus'@'172.18.x.x')
|
||||
|
||||
TCP 已通,但 MySQL 用户多为 **`nexus@localhost`**,Docker 内 Nexus 从 **172.x** 连接会被拒绝。
|
||||
|
||||
在服务器执行(按提示输入 root 密码与 nexus 密码):
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/fix-1panel-mysql-grant.sh
|
||||
```
|
||||
|
||||
或一次性:
|
||||
|
||||
```bash
|
||||
MYSQL_ROOT_PASSWORD='1Panel里MySQL的root密码' \
|
||||
NEXUS_DB_PASSWORD='向导里要填的nexus密码' \
|
||||
bash /opt/nexus/deploy/fix-1panel-mysql-grant.sh
|
||||
```
|
||||
|
||||
然后安装向导步骤 3:**库名/用户 `nexus`**,主机为 `1Panel-mysql-xxxx`,密码与上面一致。
|
||||
|
||||
若仍报 `Can't connect to MySQL/Redis on 'host.docker.internal'`:说明 Nexus **未接入 1panel-network** 或未探测到容器名,在服务器执行:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
步骤 2 会 TCP 检测 MySQL/Redis;步骤 4 验证账号密码。
|
||||
|
||||
卸载旧 Compose 容器:
|
||||
|
||||
```bash
|
||||
bash deploy/uninstall-mysql-compose.sh
|
||||
bash deploy/uninstall-redis-compose.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1Panel 反代
|
||||
|
||||
**禁止**脚本改 `/opt/1panel/apps/openresty/`。
|
||||
|
||||
1. 应用商店 → **OpenResty**
|
||||
2. 网站 → 反代 `http://127.0.0.1:8600`
|
||||
3. 参考:`deploy/1panel/openresty-nexus.conf.example`
|
||||
|
||||
---
|
||||
|
||||
## 密钥
|
||||
|
||||
新机自动生成 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY` → `docker/.env.prod`,备份 `/root/.nexus-install-secrets-*.env`。
|
||||
|
||||
MySQL 密码在 1Panel 自建库时设定,向导步骤 3 填写。
|
||||
|
||||
| 场景 | 命令 |
|
||||
|------|------|
|
||||
| 新机(推荐) | `curl \| bash` 或 `install-nexus-fresh.sh` |
|
||||
| 迁机保留旧密钥 | `nexus-1panel.sh install --from-env /path/to/old/.env` |
|
||||
| 显式复用 secrets 文件 | `nexus-1panel.sh install --reuse-secrets`(**勿用于新机**) |
|
||||
| 新机 | `curl \| quick-install.sh` |
|
||||
| 迁机 | `nexus-1panel.sh install --from-env /path/to/.env` |
|
||||
| 更新 | `update.sh` 或 `nx update` |
|
||||
|
||||
**Gitea 令牌**:公共仓库 `curl | bash` / `git clone` **不需要**配置。仅私人仓库时可选 `/root/.nexus-deploy.env` 或 `nexus-1panel.sh init-token`。
|
||||
**Git**:公开仓库 `clone` / `pull` / `curl | bash` 无需令牌。
|
||||
|
||||
## 安装向导 `/app/install.html`
|
||||
---
|
||||
|
||||
生产镜像 `Dockerfile.prod` 已内置 `install.html` + `vendor`(Alpine/Tailwind)。升级后若 404:
|
||||
## 安装向导验收(服务器执行)
|
||||
|
||||
在 **1Panel 终端**(root)一键检查网络、容器名预填、cron、镜像版本、本地 `/health`:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && sudo nx verify
|
||||
```
|
||||
|
||||
**未安装**:通过后再打开 `https://你的域名/app/install.html`。
|
||||
**已安装**:`install.html` 应 HTTP 404;验收检查 crontab、`mysql_dump_to_file.sh`、容器内 round6/7 代码特性。
|
||||
|
||||
常见修复:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
外网 HTTPS 仍失败但脚本本地检查全绿 → 在 1Panel **网站**配置反代 `http://127.0.0.1:8600`(见 `deploy/1panel/openresty-nexus.conf.example`)。
|
||||
|
||||
## 安装向导热修复
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/nexus-1panel.sh upgrade # 会 --build 重建镜像
|
||||
# 或仅同步到当前容器(旧镜像兜底):
|
||||
bash deploy/update.sh --no-cache
|
||||
# 或仅同步静态:
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
#!/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 "$@"
|
||||
+29
-85
@@ -1,102 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — MySQL Auto-Backup Script
|
||||
# Usage: Add to crontab for daily 3AM backup
|
||||
# 0 3 * * * /path/to/nexus/deploy/db_backup.sh >> /var/log/nexus_backup.log 2>&1
|
||||
# Nexus — MySQL 定时备份(Docker 1Panel / 宿主机)
|
||||
#
|
||||
# Reads DATABASE_URL from .env for connection parameters.
|
||||
# Retains backups for 30 days, auto-deletes older ones.
|
||||
|
||||
# 0 3 * * * NEXUS_ROOT=/opt/nexus /opt/nexus/deploy/db_backup.sh >> /var/log/nexus_backup.log 2>&1
|
||||
#
|
||||
# 亦可通过: sudo bash deploy/install_ops_cron.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
ENV_FILE="$PROJECT_DIR/.env"
|
||||
BACKUP_DIR="$PROJECT_DIR/backups"
|
||||
RETENTION_DAYS=30
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-$(dirname "$SCRIPT_DIR")}"
|
||||
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/var/backups/nexus}"
|
||||
RETENTION_DAYS="${NEXUS_BACKUP_RETENTION_DAYS:-30}"
|
||||
DUMP_SH="${SCRIPT_DIR}/mysql_dump_to_file.sh"
|
||||
|
||||
# ── Load .env ──
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "[$(date)] ERROR: .env not found at $ENV_FILE"
|
||||
if [[ ! -f "$DUMP_SH" ]]; then
|
||||
echo "[$(date)] ERROR: missing $DUMP_SH"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$DUMP_SH" 2>/dev/null || true
|
||||
|
||||
# Parse DATABASE_URL from .env
|
||||
# Format: mysql+asyncmy://user:password@host:port/dbname
|
||||
DB_URL=$(grep -E '^NEXUS_DATABASE_URL=' "$ENV_FILE" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")
|
||||
if [ -z "$DB_URL" ]; then
|
||||
DB_URL=$(grep -E '^DATABASE_URL=' "$ENV_FILE" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")
|
||||
fi
|
||||
|
||||
if [ -z "$DB_URL" ]; then
|
||||
echo "[$(date)] ERROR: DATABASE_URL not found in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse mysql URL: mysql+asyncmy://user:pass@host:port/dbname → user pass host port dbname
|
||||
# Strip scheme
|
||||
CONN="${DB_URL#*://}"
|
||||
# Extract user:pass
|
||||
USERPASS="${CONN%%@*}"
|
||||
DB_USER="${USERPASS%%:*}"
|
||||
DB_PASS="${USERPASS#*:}"
|
||||
# Extract host:port/dbname
|
||||
HOSTDB="${CONN#*@}"
|
||||
DB_HOST="${HOSTDB%%:*}"
|
||||
PORTDB="${HOSTDB#*:}"
|
||||
DB_PORT="${PORTDB%%/*}"
|
||||
DB_NAME="${PORTDB#*/}"
|
||||
# Remove query params
|
||||
DB_NAME="${DB_NAME%%\?*}"
|
||||
|
||||
if [ -z "$DB_NAME" ] || [ -z "$DB_HOST" ] || [ -z "$DB_USER" ]; then
|
||||
echo "[$(date)] ERROR: Failed to parse DATABASE_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Create backup directory ──
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
RAW_FILE="${BACKUP_DIR}/nexus_daily_${TIMESTAMP}.sql"
|
||||
BACKUP_FILE="${RAW_FILE}.gz"
|
||||
|
||||
# ── Backup filename ──
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="$BACKUP_DIR/nexus_${DB_NAME}_${TIMESTAMP}.sql.gz"
|
||||
|
||||
# ── Run mysqldump ──
|
||||
echo "[$(date)] Starting backup: $DB_NAME @ $DB_HOST"
|
||||
|
||||
if [ -n "$DB_PASS" ]; then
|
||||
MYSQL_PWD="$DB_PASS" mysqldump \
|
||||
-h "$DB_HOST" \
|
||||
-P "${DB_PORT:-3306}" \
|
||||
-u "$DB_USER" \
|
||||
--single-transaction \
|
||||
--routines \
|
||||
--triggers \
|
||||
--set-gtid-purged=OFF \
|
||||
"$DB_NAME" | gzip > "$BACKUP_FILE"
|
||||
else
|
||||
mysqldump \
|
||||
-h "$DB_HOST" \
|
||||
-P "${DB_PORT:-3306}" \
|
||||
-u "$DB_USER" \
|
||||
--single-transaction \
|
||||
--routines \
|
||||
--triggers \
|
||||
--set-gtid-purged=OFF \
|
||||
"$DB_NAME" | gzip > "$BACKUP_FILE"
|
||||
fi
|
||||
|
||||
# ── Verify backup ──
|
||||
if [ -f "$BACKUP_FILE" ]; then
|
||||
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||
echo "[$(date)] Backup completed: $BACKUP_FILE ($SIZE)"
|
||||
else
|
||||
echo "[$(date)] ERROR: Backup file not created!"
|
||||
echo "[$(date)] Starting backup → $BACKUP_FILE"
|
||||
if ! NEXUS_ROOT="$NEXUS_ROOT" bash "$DUMP_SH" --output "$RAW_FILE" --root "$NEXUS_ROOT"; then
|
||||
echo "[$(date)] ERROR: mysqldump failed"
|
||||
rm -f "$RAW_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Cleanup old backups (retain 30 days) ──
|
||||
DELETED=$(find "$BACKUP_DIR" -name "nexus_*.sql.gz" -mtime +${RETENTION_DAYS} -delete -print | wc -l)
|
||||
if [ "$DELETED" -gt 0 ]; then
|
||||
gzip -f "$RAW_FILE"
|
||||
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||
echo "[$(date)] ERROR: backup file not created"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SIZE="$(du -h "$BACKUP_FILE" | cut -f1)"
|
||||
echo "[$(date)] Backup completed: $BACKUP_FILE ($SIZE)"
|
||||
|
||||
DELETED="$(find "$BACKUP_DIR" -name 'nexus_*.sql.gz' -mtime +"${RETENTION_DAYS}" -delete -print 2>/dev/null | wc -l)"
|
||||
if [[ "$DELETED" -gt 0 ]]; then
|
||||
echo "[$(date)] Cleaned up $DELETED old backup(s) (>${RETENTION_DAYS} days)"
|
||||
fi
|
||||
|
||||
|
||||
+100
-34
@@ -1,58 +1,124 @@
|
||||
#!/bin/bash
|
||||
# deploy-frontend.sh — Build Vuetify frontend and deploy to server
|
||||
#!/usr/bin/env bash
|
||||
# deploy-frontend.sh — Build frontend v1 + app-v2 and deploy to server
|
||||
#
|
||||
# Docker 生产:前端在镜像内构建,远程执行 nexus-1panel.sh upgrade
|
||||
# Supervisor:tar 上传到 web/app + web/app-v2 + supervisorctl restart
|
||||
#
|
||||
# Usage: bash deploy/deploy-frontend.sh
|
||||
# Run from the Nexus project root directory.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
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}" "$@"; }
|
||||
|
||||
echo "═══ Nexus Frontend Deploy ═══"
|
||||
|
||||
# 1. Build
|
||||
echo "▶ Building frontend..."
|
||||
REMOTE_RUNTIME=$(ssh_cmd 'bash -s' <<'REMOTE'
|
||||
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
|
||||
[[ -d "$d/server" ]] || continue
|
||||
if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then
|
||||
prof="$d/docker/.host-profile"
|
||||
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
|
||||
if [[ -f "$prof" ]]; then
|
||||
args=(-f "$d/docker/docker-compose.prod.yml")
|
||||
docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \
|
||||
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
|
||||
args+=(-f "$d/docker/docker-compose.1panel.yml")
|
||||
q=$(cd "$d" && compose_cmd "${args[@]}" \
|
||||
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
|
||||
[[ -n "$q" ]] && echo "docker $d" && exit 0
|
||||
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
|
||||
echo "supervisor $d"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
echo "unknown"
|
||||
REMOTE
|
||||
)
|
||||
|
||||
RUNTIME="${REMOTE_RUNTIME%% *}"
|
||||
DEPLOY_PATH="${REMOTE_RUNTIME#* }"
|
||||
[[ "$DEPLOY_PATH" == "$REMOTE_RUNTIME" ]] && DEPLOY_PATH="/www/wwwroot/api.synaglobal.vip"
|
||||
|
||||
echo "Remote runtime: ${RUNTIME} @ ${DEPLOY_PATH}"
|
||||
|
||||
echo "▶ Building frontend v1 (local verify)..."
|
||||
cd frontend && npx vite build && cd ..
|
||||
if [[ -d frontend-v2 ]]; then
|
||||
echo "▶ Building frontend v2 (/app-v2, local verify)..."
|
||||
(cd frontend-v2 && npm run build:safe)
|
||||
test -f web/app-v2/index.html
|
||||
fi
|
||||
echo "✓ Build complete"
|
||||
|
||||
# 2. Tar the build output (index.html + assets/)
|
||||
echo "▶ Packaging..."
|
||||
tar czf /tmp/nexus-frontend.tar.gz -C web/app index.html assets/
|
||||
echo "✓ Package ready ($(du -h /tmp/nexus-frontend.tar.gz | cut -f1))"
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Docker 生产:重建镜像以包含新前端(非 tar 热更新)..."
|
||||
ssh_cmd "cd ${DEPLOY_PATH} && sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
|
||||
else
|
||||
echo "▶ Packaging web/app and web/app-v2..."
|
||||
pkg_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${pkg_dir}"' EXIT
|
||||
mkdir -p "${pkg_dir}/app"
|
||||
cp -a web/app/index.html web/app/assets "${pkg_dir}/app/"
|
||||
if [[ -f web/app-v2/index.html && -d web/app-v2/assets ]]; then
|
||||
mkdir -p "${pkg_dir}/app-v2"
|
||||
cp -a web/app-v2/index.html web/app-v2/assets "${pkg_dir}/app-v2/"
|
||||
fi
|
||||
tar czf /tmp/nexus-frontend.tar.gz -C "${pkg_dir}" .
|
||||
echo "✓ Package ready ($(du -h /tmp/nexus-frontend.tar.gz | cut -f1))"
|
||||
|
||||
# 3. Upload to server
|
||||
echo "▶ Uploading to server..."
|
||||
scp /tmp/nexus-frontend.tar.gz nexus:/tmp/nexus-frontend.tar.gz
|
||||
echo "✓ Upload complete"
|
||||
echo "▶ Uploading to server..."
|
||||
scp "${SSH_OPTS[@]}" /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
|
||||
echo "✓ Upload complete"
|
||||
|
||||
# 4. Extract on server + prune orphaned legacy chunks
|
||||
echo "▶ Deploying on server..."
|
||||
ssh nexus "cd /www/wwwroot/api.synaglobal.vip/web/app && tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz"
|
||||
echo "▶ Pruning orphaned assets (dry-run first)..."
|
||||
if ! ssh nexus "python3 /www/wwwroot/api.synaglobal.vip/deploy/prune_frontend_assets.py --dry-run"; then
|
||||
echo "✗ Prune dry-run failed — aborting (no files deleted)"
|
||||
exit 1
|
||||
echo "▶ Deploying on server..."
|
||||
ssh_cmd "set -e; mkdir -p ${DEPLOY_PATH}/web; tar xzf /tmp/nexus-frontend.tar.gz -C ${DEPLOY_PATH}/web; rm /tmp/nexus-frontend.tar.gz"
|
||||
echo "▶ Pruning orphaned assets..."
|
||||
if [[ -f deploy/prune_frontend_assets.py ]]; then
|
||||
if ! ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app --dry-run"; then
|
||||
echo "✗ Prune dry-run failed"
|
||||
exit 1
|
||||
fi
|
||||
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
|
||||
fi
|
||||
echo "✓ Extracted and pruned"
|
||||
|
||||
echo "▶ Restarting backend..."
|
||||
ssh_cmd "supervisorctl restart nexus"
|
||||
echo "✓ Restarted"
|
||||
fi
|
||||
if ! ssh nexus "python3 /www/wwwroot/api.synaglobal.vip/deploy/prune_frontend_assets.py"; then
|
||||
echo "✗ Prune failed — check index.html references before retry"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Extracted and pruned"
|
||||
|
||||
# 5. Restart backend
|
||||
echo "▶ Restarting backend..."
|
||||
ssh nexus "supervisorctl restart nexus"
|
||||
echo "✓ Restarted"
|
||||
|
||||
# 6. Health check
|
||||
sleep 3
|
||||
HEALTH=$(ssh nexus "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/health" 2>/dev/null || echo "000")
|
||||
SPA=$(ssh nexus "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/" 2>/dev/null || echo "000")
|
||||
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:8600/health" 2>/dev/null || echo "000")
|
||||
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/" 2>/dev/null || echo "000")
|
||||
SPA_V2=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app-v2/" 2>/dev/null || echo "000")
|
||||
|
||||
echo ""
|
||||
echo "═══ Verification ═══"
|
||||
echo " /health → $HEALTH"
|
||||
echo " /app/ → $SPA"
|
||||
echo " /app-v2/ → $SPA_V2"
|
||||
|
||||
if [ "$HEALTH" = "200" ] && [ "$SPA" = "200" ]; then
|
||||
if [[ "$HEALTH" == "200" || "$HEALTH" == "ok" ]] && [[ "$SPA" == "200" ]] && [[ "$SPA_V2" == "200" ]]; then
|
||||
echo "✓ Deploy successful!"
|
||||
else
|
||||
echo "✗ Verification failed — check server logs"
|
||||
|
||||
+50
-19
@@ -1,26 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run ON production server (宝塔终端 / ssh root@47.254.123.106)
|
||||
# Pulls latest main from Gitea and restarts Nexus. Frontend tar must be uploaded separately
|
||||
# if you did not build on this machine.
|
||||
# Run ON production server — 自动识别 Docker / Supervisor
|
||||
#
|
||||
# Usage (on server):
|
||||
# cd /www/wwwroot/api.synaglobal.vip
|
||||
# bash deploy/deploy-on-server.sh
|
||||
# cd /opt/nexus && sudo bash deploy/deploy-on-server.sh
|
||||
# cd /www/wwwroot/api.synaglobal.vip && bash deploy/deploy-on-server.sh
|
||||
#
|
||||
# Optional env:
|
||||
# NEXUS_GITEA_TOKEN — if git pull needs auth
|
||||
# SKIP_FRONTEND=1 — backend only
|
||||
# NEXUS_GIT_REMOTE — override authenticated remote, default: https://axs.kuma1xn.vip/admin/Nexus.git
|
||||
# SKIP_FRONTEND=1 — Supervisor 模式跳过热更新前端 tar
|
||||
# NEXUS_DEPLOY_PATH — 覆盖自动检测路径
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/www/wwwroot/api.synaglobal.vip}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./detect_deploy_runtime.sh
|
||||
source "${SCRIPT_DIR}/detect_deploy_runtime.sh"
|
||||
|
||||
DEPLOY_PATH="$(resolve_nexus_root "${NEXUS_DEPLOY_PATH:-}" 2>/dev/null || true)"
|
||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
||||
echo "ERROR: cannot resolve Nexus root" >&2
|
||||
exit 1
|
||||
fi
|
||||
cd "${DEPLOY_PATH}"
|
||||
|
||||
echo "═══ Nexus server-side deploy ═══"
|
||||
echo "Path: ${DEPLOY_PATH}"
|
||||
RUNTIME="$(detect_nexus_runtime "$DEPLOY_PATH")"
|
||||
PORT="$(nexus_publish_port_for "$DEPLOY_PATH")"
|
||||
|
||||
if [ -n "${NEXUS_GITEA_TOKEN:-}" ]; then
|
||||
git remote set-url origin "http://admin:${NEXUS_GITEA_TOKEN}@66.154.115.8:3000/admin/Nexus.git"
|
||||
echo "═══ Nexus server-side deploy ═══"
|
||||
echo "Path: ${DEPLOY_PATH}"
|
||||
echo "Runtime: ${RUNTIME}"
|
||||
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
GIT_REMOTE="${NEXUS_GIT_REMOTE:-https://axs.kuma1xn.vip/admin/Nexus.git}"
|
||||
GIT_REMOTE_AUTH="${GIT_REMOTE/https:\/\//https://admin:${NEXUS_GITEA_TOKEN}@}"
|
||||
git remote set-url origin "${GIT_REMOTE_AUTH}"
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Docker upgrade (pull + rebuild)..."
|
||||
exec env NEXUS_ROOT="$DEPLOY_PATH" bash "$DEPLOY_PATH/deploy/nexus-1panel.sh" upgrade
|
||||
fi
|
||||
|
||||
echo "▶ git fetch + reset --hard origin/main..."
|
||||
@@ -31,25 +50,37 @@ echo " HEAD: $(git log -1 --oneline)"
|
||||
echo "▶ supervisorctl restart nexus..."
|
||||
supervisorctl restart nexus
|
||||
|
||||
if [ -f /tmp/nexus-frontend.tar.gz ] && [ "${SKIP_FRONTEND:-0}" != "1" ]; then
|
||||
if [[ -f /tmp/nexus-frontend.tar.gz && "${SKIP_FRONTEND:-0}" != "1" ]]; then
|
||||
echo "▶ extract /tmp/nexus-frontend.tar.gz..."
|
||||
tar xzf /tmp/nexus-frontend.tar.gz -C web/app
|
||||
mkdir -p web/app web/app-v2
|
||||
if tar tzf /tmp/nexus-frontend.tar.gz | grep -Eq '^(\./)?app/'; then
|
||||
tar xzf /tmp/nexus-frontend.tar.gz -C web
|
||||
else
|
||||
# Backward compatibility for legacy archives containing index.html + assets/ at root.
|
||||
tar xzf /tmp/nexus-frontend.tar.gz -C web/app --exclude=app-v2
|
||||
if tar tzf /tmp/nexus-frontend.tar.gz | grep -Eq '^(\./)?app-v2/'; then
|
||||
tar xzf /tmp/nexus-frontend.tar.gz -C web/app-v2 --strip-components=1 app-v2
|
||||
fi
|
||||
fi
|
||||
rm -f /tmp/nexus-frontend.tar.gz
|
||||
if [ -f deploy/prune_frontend_assets.py ]; then
|
||||
python3 deploy/prune_frontend_assets.py --dry-run
|
||||
python3 deploy/prune_frontend_assets.py
|
||||
if [[ -f deploy/prune_frontend_assets.py ]]; then
|
||||
python3 deploy/prune_frontend_assets.py web/app --dry-run
|
||||
python3 deploy/prune_frontend_assets.py web/app
|
||||
fi
|
||||
supervisorctl restart nexus
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
HEALTH=$(curl -s http://127.0.0.1:8600/health || true)
|
||||
SPA=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/ || echo "000")
|
||||
HEALTH=$(curl -s "http://127.0.0.1:${PORT}/health" || true)
|
||||
SPA=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/app/" || echo "000")
|
||||
SPA_V2=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/app-v2/" || echo "000")
|
||||
echo " /health → ${HEALTH}"
|
||||
echo " /app/ → ${SPA}"
|
||||
echo " /app-v2/ → ${SPA_V2}"
|
||||
|
||||
if [ "${HEALTH}" = "ok" ] && [ "${SPA}" = "200" ]; then
|
||||
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" && "${SPA_V2}" == "200" ]]; then
|
||||
echo "✓ Server deploy OK"
|
||||
suggest_ops_cron "$DEPLOY_PATH"
|
||||
else
|
||||
echo "✗ Verify failed — check: supervisorctl status nexus" >&2
|
||||
exit 1
|
||||
|
||||
+162
-21
@@ -1,35 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Full production deploy: push (optional) + backend pull + frontend tar + health
|
||||
# Full production deploy: local rsync + backend upgrade + frontend + health
|
||||
#
|
||||
# 默认从本机工作区 rsync 到生产机,不 push Gitea、不远程 git pull。
|
||||
#
|
||||
# Prerequisites on YOUR machine:
|
||||
# - SSH: ~/.ssh/config Host nexus OR export NEXUS_SSH="root@47.254.123.106"
|
||||
# - rsync, SSH Host nexus OR export NEXUS_SSH="azureuser@20.24.218.235"
|
||||
# - Key: export NEXUS_SSH_KEY=~/.ssh/id_rsa_nexus
|
||||
# - Gitea push access for origin main
|
||||
#
|
||||
# Usage:
|
||||
# cd "$NEXUS_ROOT"
|
||||
# git add ... && git commit ... && git push origin main # if not pushed yet
|
||||
# bash deploy/pre_deploy_check.sh
|
||||
# bash deploy/deploy-production.sh
|
||||
#
|
||||
# 仍走 Gitea(仅当显式设置):
|
||||
# NEXUS_DEPLOY_VIA_GIT=1 bash deploy/deploy-production.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
|
||||
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
|
||||
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
|
||||
fi
|
||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||
scp_cmd() { scp "${SSH_OPTS[@]}" "$@"; }
|
||||
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/www/wwwroot/api.synaglobal.vip}"
|
||||
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 "SSH target: ${NEXUS_SSH_HOST}"
|
||||
echo "Deploy path: ${DEPLOY_PATH}"
|
||||
echo "Mode: $([[ "$DEPLOY_VIA_GIT" == 1 ]] && echo 'Gitea git pull' || echo 'local rsync')"
|
||||
|
||||
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
||||
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
||||
@@ -37,27 +50,155 @@ if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "▶ Backend: git pull + restart..."
|
||||
ssh_cmd "cd ${DEPLOY_PATH} && git fetch --all && git reset --hard origin/main && supervisorctl restart nexus"
|
||||
REMOTE_DETECT=$(ssh_cmd 'bash -s' <<'REMOTE'
|
||||
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
|
||||
[[ -d "$d/server" ]] || continue
|
||||
echo "ROOT=$d"
|
||||
if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then
|
||||
prof="$d/docker/.host-profile"
|
||||
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
|
||||
if [[ -f "$prof" ]]; then
|
||||
args=(-f "$d/docker/docker-compose.prod.yml")
|
||||
docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \
|
||||
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
|
||||
args+=(-f "$d/docker/docker-compose.1panel.yml")
|
||||
q=$(cd "$d" && compose_cmd "${args[@]}" \
|
||||
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
|
||||
[[ -n "$q" ]] && echo RUNTIME=docker && exit 0
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$d/deploy/nexus-1panel.sh" ]] && command -v nx >/dev/null 2>&1; then
|
||||
q=$(docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E '^nexus-prod-nexus-' || true)
|
||||
[[ -n "$q" ]] && echo RUNTIME=docker && exit 0
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo RUNTIME=supervisor
|
||||
exit 0
|
||||
fi
|
||||
echo RUNTIME=unknown
|
||||
exit 0
|
||||
done
|
||||
echo RUNTIME=none
|
||||
REMOTE
|
||||
)
|
||||
|
||||
echo "▶ Frontend: build + upload..."
|
||||
cd frontend && npx vite build && cd ..
|
||||
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"
|
||||
ssh_cmd "cd ${DEPLOY_PATH}/web/app && tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz"
|
||||
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
|
||||
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py --dry-run"
|
||||
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-$(echo "$REMOTE_DETECT" | sed -n 's/^ROOT=//p' | head -1)}"
|
||||
RUNTIME="$(echo "$REMOTE_DETECT" | sed -n 's/^RUNTIME=//p' | head -1)"
|
||||
|
||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
||||
fi
|
||||
if [[ -z "$RUNTIME" || "$RUNTIME" == "none" ]]; then
|
||||
RUNTIME="unknown"
|
||||
fi
|
||||
|
||||
echo "Deploy path: ${DEPLOY_PATH}"
|
||||
echo "Runtime: ${RUNTIME}"
|
||||
|
||||
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 "File owner: ${DEPLOY_CHOWN}"
|
||||
|
||||
if [[ "$DEPLOY_VIA_GIT" == 1 ]]; then
|
||||
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 and web/app-v2..."
|
||||
cd frontend && npx vite build && cd ..
|
||||
if [[ -d frontend-v2 ]]; then
|
||||
(cd frontend-v2 && npm run build:safe)
|
||||
test -f web/app-v2/index.html
|
||||
fi
|
||||
pkg_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${pkg_dir}"' EXIT
|
||||
mkdir -p "${pkg_dir}/app"
|
||||
cp -a web/app/index.html web/app/assets "${pkg_dir}/app/"
|
||||
if [[ -f web/app-v2/index.html && -d web/app-v2/assets ]]; then
|
||||
mkdir -p "${pkg_dir}/app-v2"
|
||||
cp -a web/app-v2/index.html web/app-v2/assets "${pkg_dir}/app-v2/"
|
||||
fi
|
||||
tar czf /tmp/nexus-frontend.tar.gz -C "${pkg_dir}" .
|
||||
scp_cmd /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
|
||||
ssh_cmd "set -e; sudo mkdir -p ${DEPLOY_PATH}/web; sudo tar xzf /tmp/nexus-frontend.tar.gz -C ${DEPLOY_PATH}/web; rm /tmp/nexus-frontend.tar.gz; sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app ${DEPLOY_PATH}/web/app-v2"
|
||||
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 "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Sync host web/app and web/app-v2 into container..."
|
||||
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 + web/app-v2 sync failed — run sync_webapp_to_container.sh on server manually" >&2
|
||||
}
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:8600/health" 2>/dev/null || true)
|
||||
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/" 2>/dev/null || echo "000")
|
||||
PORT="$(ssh_cmd "grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod 2>/dev/null | cut -d= -f2 | tr -d '\"'" 2>/dev/null || true)"
|
||||
PORT="${PORT:-8600}"
|
||||
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_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 " /app/ → ${SPA}"
|
||||
if [ "${HEALTH}" = "ok" ] && [ "${SPA}" = "200" ]; then
|
||||
echo " /app-v2/ → ${SPA_V2}"
|
||||
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" && "${SPA_V2}" == "200" ]]; then
|
||||
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
|
||||
echo "✓ Ops cron (health_monitor) already installed"
|
||||
else
|
||||
echo "▶ Installing ops cron (health_monitor + db_backup)…"
|
||||
if ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/install_ops_cron.sh" 2>/dev/null; then
|
||||
echo "✓ Ops cron installed"
|
||||
else
|
||||
echo " 提示: 远程 cron 安装失败,可在服务器执行: sudo nx cron"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "✗ Verification failed — check supervisor logs on server" >&2
|
||||
echo "✗ Verification failed — check logs on server" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# Detect 1Panel App Store MySQL/Redis container names on 1panel-network.
|
||||
# See: https://github.com/1Panel-dev/1Panel/issues/11676
|
||||
# https://github.com/1Panel-dev/appstore (mysql/redis → networks: 1panel-network)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
network_container_names() {
|
||||
docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null \
|
||||
| tr ' ' '\n' | sed '/^$/d'
|
||||
}
|
||||
|
||||
pick_service_container() {
|
||||
local kind="$1"
|
||||
local names line
|
||||
names="$(network_container_names)"
|
||||
if [[ -z "$names" ]]; then
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "(^1Panel-${kind}-|${kind})" | head -1 || true
|
||||
return
|
||||
fi
|
||||
line="$(echo "$names" | grep -iE "^1Panel-${kind}-" | head -1 || true)"
|
||||
if [[ -n "$line" ]]; then
|
||||
echo "$line"
|
||||
return
|
||||
fi
|
||||
echo "$names" | grep -i "$kind" | head -1 || true
|
||||
}
|
||||
|
||||
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
echo "1panel-network not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MYSQL_HOST="$(pick_service_container mysql)"
|
||||
REDIS_HOST="$(pick_service_container redis)"
|
||||
|
||||
if [[ -z "$MYSQL_HOST" && -z "$REDIS_HOST" ]]; then
|
||||
echo "No MySQL/Redis containers on 1panel-network" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
[[ -n "$MYSQL_HOST" ]] && echo "NEXUS_1PANEL_DB_HOST=${MYSQL_HOST}"
|
||||
[[ -n "$REDIS_HOST" ]] && echo "NEXUS_1PANEL_REDIS_HOST=${REDIS_HOST}"
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — 部署运行时检测(Docker 1Panel / Supervisor 裸机)
|
||||
#
|
||||
# source deploy/detect_deploy_runtime.sh
|
||||
# ROOT="$(resolve_nexus_root)"
|
||||
# RUNTIME="$(detect_nexus_runtime "$ROOT")" # docker | supervisor | unknown
|
||||
set -euo pipefail
|
||||
|
||||
_COMPOSE_FILE="docker/docker-compose.prod.yml"
|
||||
_COMPOSE_1PANEL="docker/docker-compose.1panel.yml"
|
||||
_ENV_PROD="docker/.env.prod"
|
||||
|
||||
resolve_nexus_root() {
|
||||
local hint="${1:-}"
|
||||
if [[ -n "$hint" && -d "$hint/server" ]]; then
|
||||
echo "$hint"
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${NEXUS_ROOT:-}" && -d "${NEXUS_ROOT}/server" ]]; then
|
||||
echo "$NEXUS_ROOT"
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${NEXUS_DEPLOY_PATH:-}" && -d "${NEXUS_DEPLOY_PATH}/server" ]]; then
|
||||
echo "$NEXUS_DEPLOY_PATH"
|
||||
return 0
|
||||
fi
|
||||
local d
|
||||
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
|
||||
if [[ -d "$d/server" ]]; then
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
for d in /www/wwwroot/*/; do
|
||||
[[ -d "${d}server" ]] || continue
|
||||
echo "${d%/}"
|
||||
return 0
|
||||
done
|
||||
if command -v supervisorctl >/dev/null 2>&1; then
|
||||
local cwd
|
||||
cwd="$(supervisorctl status nexus 2>/dev/null | grep -oP 'cwd=\K[^ ,]+' || true)"
|
||||
if [[ -n "$cwd" && -d "$cwd/server" ]]; then
|
||||
echo "$cwd"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
_has_1panel_network() {
|
||||
docker network inspect 1panel-network >/dev/null 2>&1
|
||||
}
|
||||
|
||||
detect_nexus_runtime() {
|
||||
local root="$1"
|
||||
if [[ -f "$root/$_ENV_PROD" ]] && command -v docker >/dev/null 2>&1 \
|
||||
&& docker compose version >/dev/null 2>&1; then
|
||||
local -a args=(-f "$root/$_COMPOSE_FILE")
|
||||
if _has_1panel_network && [[ -f "$root/$_COMPOSE_1PANEL" ]]; then
|
||||
args+=(-f "$root/$_COMPOSE_1PANEL")
|
||||
fi
|
||||
local prof q
|
||||
prof="$root/docker/.host-profile"
|
||||
[[ -f "$prof" ]] || prof="$root/docker/profiles/2c8g.env"
|
||||
if [[ -f "$prof" ]]; then
|
||||
q="$(cd "$root" && docker compose "${args[@]}" \
|
||||
--env-file "$root/$_ENV_PROD" --env-file "$prof" \
|
||||
ps -q nexus 2>/dev/null || true)"
|
||||
if [[ -n "$q" ]]; then
|
||||
echo docker
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 \
|
||||
&& supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo supervisor
|
||||
return 0
|
||||
fi
|
||||
echo unknown
|
||||
}
|
||||
|
||||
nexus_publish_port_for() {
|
||||
local root="$1"
|
||||
local port
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$root/$_ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
if [[ -z "$port" && -f "$root/.env" ]]; then
|
||||
port="$(grep -E '^NEXUS_PORT=' "$root/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
fi
|
||||
echo "${port:-8600}"
|
||||
}
|
||||
|
||||
nexus_restart_service() {
|
||||
local root="$1"
|
||||
local runtime
|
||||
runtime="$(detect_nexus_runtime "$root")"
|
||||
case "$runtime" in
|
||||
docker)
|
||||
local cname
|
||||
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -n "$cname" ]]; then
|
||||
docker restart "$cname"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
;;
|
||||
supervisor)
|
||||
supervisorctl restart nexus
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
nexus_has_ops_cron() {
|
||||
command -v crontab >/dev/null 2>&1 \
|
||||
&& crontab -l 2>/dev/null | grep -q 'nexus-health-monitor'
|
||||
}
|
||||
|
||||
suggest_ops_cron() {
|
||||
local root="$1"
|
||||
if nexus_has_ops_cron; then
|
||||
return 0
|
||||
fi
|
||||
echo ""
|
||||
echo "[WARN] 未检测到 health_monitor / db_backup cron"
|
||||
echo " 建议: sudo bash ${root}/deploy/install_ops_cron.sh"
|
||||
echo " 或: sudo nx cron"
|
||||
}
|
||||
|
||||
prompt_ops_cron_if_missing() {
|
||||
local root="$1"
|
||||
if nexus_has_ops_cron; then
|
||||
echo "[INFO] Crontab 已含 health_monitor / db_backup"
|
||||
return 0
|
||||
fi
|
||||
suggest_ops_cron "$root"
|
||||
if [[ ! -t 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
local ans
|
||||
read -r -p "现在安装巡检/备份 cron? [Y/n]: " ans
|
||||
if [[ "$ans" == "n" || "$ans" == "N" ]]; then
|
||||
echo "[WARN] 已跳过 cron 安装"
|
||||
return 0
|
||||
fi
|
||||
bash "${root}/deploy/install_ops_cron.sh"
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# 下载并执行 install-nexus-fresh.sh(公共仓库无需令牌)
|
||||
#
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/download-install-fresh.sh" | bash
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/download-install-fresh.sh" | bash
|
||||
#
|
||||
# 推荐直接使用:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
|
||||
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
OUT="${OUT:-/tmp/install-nexus-fresh.sh}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
@@ -26,7 +27,7 @@ done
|
||||
|
||||
download() {
|
||||
local url base
|
||||
for base in "${NEXUS_RAW_BASE}" "http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
for base in "${NEXUS_RAW_BASE}" "${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/install-nexus-fresh.sh"
|
||||
if curl -fsSL "$url" -o "$OUT" 2>/dev/null && head -1 "$OUT" | grep -q '^#!/'; then
|
||||
ok "已下载 install-nexus-fresh.sh"
|
||||
@@ -35,7 +36,7 @@ download() {
|
||||
fi
|
||||
done
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
url="http://${NEXUS_GITEA_HOST}/api/v1/repos/admin/Nexus/raw/deploy/install-nexus-fresh.sh?ref=${NEXUS_GIT_BRANCH}"
|
||||
url="${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/api/v1/repos/admin/Nexus/raw/deploy/install-nexus-fresh.sh?ref=${NEXUS_GIT_BRANCH}"
|
||||
if curl -fsSL -H "Authorization: token ${NEXUS_GITEA_TOKEN}" "$url" -o "$OUT" 2>/dev/null \
|
||||
&& head -1 "$OUT" | grep -q '^#!/'; then
|
||||
ok "已下载(API + 令牌)"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Grant nexus@'%' on 1Panel App Store MySQL for Docker (1panel-network) install wizard.
|
||||
# Fixes: Access denied for user 'nexus'@'172.18.x.x' (1045)
|
||||
#
|
||||
# Usage:
|
||||
# MYSQL_ROOT_PASSWORD='root密码' NEXUS_DB_PASSWORD='nexus密码' bash deploy/fix-1panel-mysql-grant.sh
|
||||
# bash deploy/fix-1panel-mysql-grant.sh # prompts
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_DB_NAME="${NEXUS_DB_NAME:-nexus}"
|
||||
NEXUS_DB_USER="${NEXUS_DB_USER:-nexus}"
|
||||
|
||||
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; exit 1; }
|
||||
|
||||
pick_mysql_container() {
|
||||
local name
|
||||
if [[ -n "${MYSQL_CONTAINER:-}" ]]; then
|
||||
echo "$MYSQL_CONTAINER"
|
||||
return 0
|
||||
fi
|
||||
if docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
name="$(docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' \
|
||||
| tr ' ' '\n' | grep -iE '^1Panel-mysql-' | head -1 || true)"
|
||||
[[ -n "$name" ]] && echo "$name" && return 0
|
||||
fi
|
||||
name="$(docker ps --format '{{.Names}}' | grep -iE '^1Panel-mysql-' | head -1 || true)"
|
||||
[[ -n "$name" ]] && echo "$name" && return 0
|
||||
error "未找到 1Panel MySQL 容器(可设 MYSQL_CONTAINER=名称)"
|
||||
}
|
||||
|
||||
prompt_secret() {
|
||||
local var="$1" prompt="$2"
|
||||
if [[ -n "${!var:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
read -r -s -p "$prompt: " "$var"
|
||||
echo ""
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
error "$var 不能为空"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
[[ "${EUID:-$(id -u)}" -eq 0 ]] || error "请用 root 运行"
|
||||
|
||||
local mysql_c
|
||||
mysql_c="$(pick_mysql_container)"
|
||||
info "MySQL 容器: $mysql_c"
|
||||
|
||||
prompt_secret MYSQL_ROOT_PASSWORD "1Panel MySQL root 密码"
|
||||
prompt_secret NEXUS_DB_PASSWORD "nexus 用户密码(与安装向导步骤 3 一致)"
|
||||
|
||||
info "创建库/用户并授权 ${NEXUS_DB_USER}@'%' → ${NEXUS_DB_NAME}.* ..."
|
||||
|
||||
docker exec -i "$mysql_c" mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" <<SQL
|
||||
CREATE DATABASE IF NOT EXISTS \`${NEXUS_DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER IF NOT EXISTS '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
|
||||
ALTER USER '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
|
||||
GRANT ALL PRIVILEGES ON \`${NEXUS_DB_NAME}\`.* TO '${NEXUS_DB_USER}'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
SELECT user, host FROM mysql.user WHERE user = '${NEXUS_DB_USER}';
|
||||
SQL
|
||||
|
||||
info "完成。请在安装向导步骤 3 使用: 库/用户=${NEXUS_DB_NAME}/${NEXUS_DB_USER},密码与上面一致"
|
||||
info "若已改 install.py,执行: bash deploy/sync-install-wizard-to-container.sh"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
File diff suppressed because it is too large
Load Diff
+317
-56
@@ -1,70 +1,331 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — External Health Monitor (Layer 3 of 3-tier guardian)
|
||||
# Runs via crontab every minute.
|
||||
# Does NOT depend on Python — even if Python completely crashes, this script works.
|
||||
# Runs via crontab every minute. Works on Docker (1Panel) and Supervisor bare-metal.
|
||||
#
|
||||
# Guardian layers:
|
||||
# Layer 1: Supervisor — auto-restart crashed Python process
|
||||
# Layer 2: Python self_monitor — internal Redis/MySQL/WS checks (30s)
|
||||
# Layer 3: Shell health_monitor.sh — external HTTP health check (1min cron)
|
||||
#
|
||||
# Flow:
|
||||
# 1. HTTP GET http://127.0.0.1:8600/health
|
||||
# 2. If fails >= 3 consecutive times → restart via supervisorctl + Telegram alert
|
||||
# 3. If restart succeeds → Telegram recovery notification
|
||||
# 4. If restart fails → Telegram "needs human intervention" alert
|
||||
#
|
||||
# *** Configure DEPLOY_DIR and NEXUS_SERVICE before deploying ***
|
||||
# Layer 1: Docker restart policy / Supervisor autorestart
|
||||
# Layer 2: Python self_monitor (30s)
|
||||
# Layer 3: This script — HTTP /health, 3 failures → restart + optional Telegram
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-/opt/nexus}"
|
||||
NEXUS_SERVICE="${NEXUS_SERVICE:-nexus}"
|
||||
FAIL_FILE="/tmp/nexus_health_fail_count"
|
||||
STATE_DIR=""
|
||||
FAIL_FILE=""
|
||||
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
|
||||
HEALTH_URL="http://127.0.0.1:8600/health"
|
||||
RESTART_COOLDOWN_SEC="${NEXUS_HEALTH_RESTART_COOLDOWN:-300}"
|
||||
ENV_PROD="${DEPLOY_DIR}/docker/.env.prod"
|
||||
|
||||
# ── Read Telegram config from .env file ──
|
||||
DOTENV="$DEPLOY_DIR/.env"
|
||||
|
||||
BOT_TOKEN=""
|
||||
CHAT_ID=""
|
||||
|
||||
if [ -f "$DOTENV" ]; then
|
||||
BOT_TOKEN=$(grep '^NEXUS_TELEGRAM_BOT_TOKEN=' "$DOTENV" 2>/dev/null | cut -d'=' -f2- || echo "")
|
||||
CHAT_ID=$(grep '^NEXUS_TELEGRAM_CHAT_ID=' "$DOTENV" 2>/dev/null | cut -d'=' -f2- || echo "")
|
||||
fi
|
||||
|
||||
send_telegram() {
|
||||
[ -z "$BOT_TOKEN" ] || [ -z "$CHAT_ID" ] && return 1
|
||||
curl -sf --max-time 10 "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d chat_id="$CHAT_ID" \
|
||||
-d text="$1" \
|
||||
-d parse_mode=HTML \
|
||||
> /dev/null 2>&1
|
||||
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"
|
||||
}
|
||||
|
||||
# ── Health Check ──
|
||||
if ! curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
# Python is NOT responding
|
||||
FAIL=$(cat "$FAIL_FILE" 2>/dev/null || echo "0")
|
||||
FAIL=$((FAIL + 1))
|
||||
echo "$FAIL" > "$FAIL_FILE"
|
||||
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
|
||||
}
|
||||
|
||||
if [ "$FAIL" -ge "$MAX_FAIL" ]; then
|
||||
# Consecutive failures >= threshold → attempt restart
|
||||
send_telegram "🔴 <b>Nexus后端连续${MAX_FAIL}次无响应</b>\n正在通过Supervisor自动重启..."
|
||||
supervisorctl restart "$NEXUS_SERVICE" > /dev/null 2>&1
|
||||
sleep 5
|
||||
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
|
||||
}
|
||||
|
||||
# Verify restart
|
||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
send_telegram "🟢 <b>Nexus后端已恢复</b>\nSupervisor重启成功"
|
||||
echo "0" > "$FAIL_FILE"
|
||||
else
|
||||
send_telegram "🔴 <b>Nexus重启后仍未恢复!请人工介入</b>\nSupervisor重启失败,需要手动检查"
|
||||
# Don't reset fail count — keep retrying each cron cycle
|
||||
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() {
|
||||
local port
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
echo "${port:-8600}"
|
||||
}
|
||||
|
||||
PUBLISH_PORT="$(resolve_publish_port)"
|
||||
HEALTH_URL="http://127.0.0.1:${PUBLISH_PORT}/health"
|
||||
|
||||
resolve_nexus_container() {
|
||||
docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||||
}
|
||||
|
||||
read_env_var() {
|
||||
local key="$1" file="$2"
|
||||
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() {
|
||||
local cname="$1"
|
||||
[[ -n "$cname" ]] || return 1
|
||||
[[ -n "${BOT_TOKEN:-}" && -n "${CHAT_ID:-}" ]] && return 0
|
||||
local out token chat
|
||||
out="$(docker_cmd exec "$cname" python3 -c "
|
||||
import asyncio
|
||||
from server.config import settings as s
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
|
||||
async def main():
|
||||
async with AsyncSessionLocal() as db:
|
||||
await s.load_settings_from_db(db)
|
||||
t = (s.TELEGRAM_BOT_TOKEN or '').strip()
|
||||
c = (s.TELEGRAM_CHAT_ID or '').strip()
|
||||
if t and c:
|
||||
print(t)
|
||||
print(c)
|
||||
|
||||
asyncio.run(main())
|
||||
" 2>/dev/null)" || true
|
||||
[[ -n "$out" ]] || return 1
|
||||
token="$(printf '%s\n' "$out" | sed -n '1p')"
|
||||
chat="$(printf '%s\n' "$out" | sed -n '2p')"
|
||||
[[ -n "$token" && -n "$chat" ]] || return 1
|
||||
BOT_TOKEN="$token"
|
||||
CHAT_ID="$chat"
|
||||
}
|
||||
|
||||
load_telegram_config() {
|
||||
local cname="$1" dotenv
|
||||
BOT_TOKEN=""
|
||||
CHAT_ID=""
|
||||
for dotenv in \
|
||||
"${DEPLOY_DIR}/.env" \
|
||||
"$ENV_PROD" \
|
||||
; do
|
||||
[[ -f "$dotenv" ]] || continue
|
||||
BOT_TOKEN="$(read_env_var NEXUS_TELEGRAM_BOT_TOKEN "$dotenv")"
|
||||
CHAT_ID="$(read_env_var NEXUS_TELEGRAM_CHAT_ID "$dotenv")"
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
|
||||
done
|
||||
if [[ -n "$cname" ]]; then
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
NEXUS_TELEGRAM_BOT_TOKEN=*) BOT_TOKEN="${line#NEXUS_TELEGRAM_BOT_TOKEN=}" ;;
|
||||
NEXUS_TELEGRAM_CHAT_ID=*) CHAT_ID="${line#NEXUS_TELEGRAM_CHAT_ID=}" ;;
|
||||
esac
|
||||
done < <(docker_cmd exec "$cname" grep -E '^NEXUS_TELEGRAM_' /app/.env 2>/dev/null || true)
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
|
||||
load_telegram_from_db "$cname" || true
|
||||
fi
|
||||
BOT_TOKEN="$(normalize_env_value "$BOT_TOKEN")"
|
||||
CHAT_ID="$(normalize_env_value "$CHAT_ID")"
|
||||
}
|
||||
|
||||
send_telegram() {
|
||||
local msg="${1}" resp http log="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||
BOT_TOKEN="$(normalize_env_value "${BOT_TOKEN:-}")"
|
||||
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}" \
|
||||
--data-urlencode "text=${msg}" \
|
||||
-d "parse_mode=HTML" 2>/dev/null)" || return 1
|
||||
http="${resp##*$'\n'}"
|
||||
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() {
|
||||
local fail=0
|
||||
init_layer3_state
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
(
|
||||
flock -x 9
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
fail=$((fail + 1))
|
||||
write_state_file "$FAIL_FILE" "$fail" || true
|
||||
) 9>"${FAIL_FILE}.lock"
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
else
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
fail=$((fail + 1))
|
||||
write_state_file "$FAIL_FILE" "$fail" || true
|
||||
fi
|
||||
echo "$fail"
|
||||
}
|
||||
|
||||
reset_fail_count() {
|
||||
write_state_file "$FAIL_FILE" "0" || true
|
||||
}
|
||||
|
||||
restart_cooldown_active() {
|
||||
local now last elapsed
|
||||
[[ -f "$LAST_RESTART_FILE" ]] || return 1
|
||||
now="$(date +%s)"
|
||||
last="$(cat "$LAST_RESTART_FILE" 2>/dev/null || echo 0)"
|
||||
elapsed=$((now - last))
|
||||
[[ "$elapsed" -lt "$RESTART_COOLDOWN_SEC" ]]
|
||||
}
|
||||
|
||||
mark_restart_attempt() {
|
||||
write_state_file "$LAST_RESTART_FILE" "$(date +%s)" || true
|
||||
}
|
||||
|
||||
restart_nexus() {
|
||||
local cname="$1" via=""
|
||||
if [[ -n "$cname" ]]; then
|
||||
docker_cmd 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
|
||||
supervisorctl restart "$NEXUS_SERVICE" >/dev/null 2>&1 && via="Supervisor ${NEXUS_SERVICE}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
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)"
|
||||
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
|
||||
reset_fail_count
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FAIL="$(bump_fail_count)"
|
||||
if [[ "$FAIL" -lt "$MAX_FAIL" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if restart_cooldown_active; then
|
||||
telegram_health_fail_cooldown
|
||||
exit 0
|
||||
fi
|
||||
|
||||
telegram_health_fail_restart
|
||||
mark_restart_attempt
|
||||
|
||||
if VIA="$(restart_nexus "$NEXUS_CONTAINER")"; then
|
||||
sleep 5
|
||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
telegram_health_recovered "$VIA"
|
||||
reset_fail_count
|
||||
else
|
||||
telegram_health_still_down "$VIA"
|
||||
fi
|
||||
else
|
||||
# Python is responding normally
|
||||
echo "0" > "$FAIL_FILE"
|
||||
telegram_health_restart_failed
|
||||
fi
|
||||
|
||||
Regular → Executable
+5
-4
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# 适用:全新 VPS / 重装系统后,root 一条命令装完整栈。
|
||||
#
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
#
|
||||
# 4 核 16G + 自定义域名:
|
||||
# curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g --domain api.example.com
|
||||
@@ -23,9 +23,10 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
|
||||
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
@@ -233,7 +234,7 @@ download_fresh_installer() {
|
||||
local name="install-nexus-fresh.sh" dest
|
||||
dest="$(mktemp /tmp/install-nexus-fresh.XXXXXX.sh)"
|
||||
local url base
|
||||
for base in "${NEXUS_RAW_BASE}" "http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
for base in "${NEXUS_RAW_BASE}" "${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/${name}"
|
||||
if curl -fsSL "$url" -o "$dest" 2>/dev/null && head -1 "$dest" | grep -q '^#!/'; then
|
||||
chmod +x "$dest"
|
||||
|
||||
Regular → Executable
+24
-23
@@ -7,7 +7,7 @@
|
||||
#
|
||||
# 用法(公共仓库,root 下一条命令,参考 1Panel):
|
||||
#
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
#
|
||||
# 已 clone 到 /opt/nexus:
|
||||
# cd /opt/nexus && bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
@@ -32,12 +32,13 @@ fi
|
||||
|
||||
# ── 默认配置(与 SECRETS.md / nexus-1panel 一致)──
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
|
||||
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://66.154.115.8:3000/admin/Nexus/raw/branch/main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
@@ -66,7 +67,7 @@ Nexus 6.0 全新安装(1Panel + Docker)
|
||||
-h, --help 显示帮助
|
||||
|
||||
安装后请用浏览器打开: https://域名/app/install.html
|
||||
root 下全局命令: nexus-fresh | nx | nx god
|
||||
root 下全局命令: nexus-fresh | nexus-update | nx
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -120,20 +121,8 @@ load_credentials() {
|
||||
|
||||
install_root_commands() {
|
||||
[[ -d "${NEXUS_ROOT}/deploy" ]] || return 0
|
||||
chmod +x "${NEXUS_ROOT}/deploy/nx" \
|
||||
"${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" \
|
||||
"${NEXUS_ROOT}/deploy/nexus-1panel.sh" \
|
||||
"${NEXUS_ROOT}/deploy/sync-install-wizard-to-container.sh" 2>/dev/null || true
|
||||
ln -sf "${NEXUS_ROOT}/deploy/nx" /usr/local/bin/nx
|
||||
ln -sf "${NEXUS_ROOT}/deploy/nexus-1panel.sh" /usr/local/bin/nexus-1panel
|
||||
ln -sf "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" /usr/local/bin/nexus-fresh
|
||||
ln -sf "${NEXUS_ROOT}/deploy/quick-install.sh" /usr/local/bin/nexus-install
|
||||
ln -sf "${NEXUS_ROOT}/deploy/install-1panel-docker.sh" /usr/local/bin/nexus-1panel-docker 2>/dev/null || true
|
||||
info "已注册全局命令(root 直接输入即可):"
|
||||
info " nexus-1panel-docker — 重装服务器(1Panel + Docker + Nexus)"
|
||||
info " nexus-install — 仅 Nexus Docker(curl 入口)"
|
||||
info " nx — 主菜单 / 上帝菜单"
|
||||
info " nexus-fresh — 全新安装脚本"
|
||||
NEXUS_ROOT="$NEXUS_ROOT" bash "${NEXUS_ROOT}/deploy/install-nx-cli.sh" || true
|
||||
info "全局命令: nx · nexus-update · nexus-fresh · nexus-install · nexus-1panel"
|
||||
}
|
||||
|
||||
phase_preflight() {
|
||||
@@ -207,9 +196,9 @@ phase_packages() {
|
||||
|
||||
git_clone_url() {
|
||||
if [[ -z "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
echo "${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
else
|
||||
echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
echo "${NEXUS_GITEA_SCHEME}://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -286,18 +275,30 @@ phase_post() {
|
||||
echo -e "${GREEN}║${NC} 目标: http://127.0.0.1:${port} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} 示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}③ 日常运维(上帝菜单)${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nx god # 重启 Python / 升级 / 日志 / 备份 ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nx # 主菜单 ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}③ 日常运维${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nx # 统一菜单(一键更新 / 重启 / 日志) ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nexus-update # 等同 deploy/update.sh ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
warn_if_already_installed() {
|
||||
local env_prod="${NEXUS_ROOT}/docker/.env.prod" cname
|
||||
[[ -f "$env_prod" ]] || return 0
|
||||
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -n "$cname" ]] && docker exec "$cname" test -f /var/lib/nexus/.install_locked 2>/dev/null; then
|
||||
warn "检测到已安装并锁定的 Nexus(将清除 nexus-state 卷内配置并重建)"
|
||||
read -r -p "确认继续全新安装? [y/N]: " ok
|
||||
[[ "$ok" == "y" || "$ok" == "Y" ]] || exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
banner
|
||||
if [[ "$SKIP_CLONE" == true && -d "${NEXUS_ROOT}/deploy" ]]; then
|
||||
install_root_commands
|
||||
fi
|
||||
warn_if_already_installed
|
||||
phase_preflight
|
||||
phase_1panel
|
||||
phase_packages
|
||||
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# 注册 Nexus 全局 CLI(/usr/local/bin)— install / upgrade / nx 均会调用
|
||||
#
|
||||
# NEXUS_ROOT=/opt/nexus bash deploy/install-nx-cli.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
BIN_DIR="${NEXUS_BIN_DIR:-/usr/local/bin}"
|
||||
DEPLOY="${NEXUS_ROOT}/deploy"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
if [[ ! -d "$DEPLOY" ]]; then
|
||||
error "未找到 $DEPLOY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
warn "非 root:无法写入 $BIN_DIR,请使用 root 执行或: sudo bash $DEPLOY/install-nx-cli.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
chmod +x \
|
||||
"$DEPLOY/nx" \
|
||||
"$DEPLOY/update.sh" \
|
||||
"$DEPLOY/nexus-1panel.sh" \
|
||||
"$DEPLOY/install-nexus-fresh.sh" \
|
||||
"$DEPLOY/quick-install.sh" \
|
||||
"$DEPLOY/install-1panel-docker.sh" \
|
||||
"$DEPLOY/upgrade-1panel-docker.sh" \
|
||||
"$DEPLOY/sync-install-wizard-to-container.sh" \
|
||||
"$DEPLOY/uninstall-mysql-compose.sh" \
|
||||
"$DEPLOY/uninstall-redis-compose.sh" \
|
||||
"$DEPLOY/install-nx-cli.sh" \
|
||||
2>/dev/null || true
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
link_cli() {
|
||||
local src="$1" name="$2"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
warn "跳过 $name(缺少 $src)"
|
||||
return 0
|
||||
fi
|
||||
ln -sf "$src" "${BIN_DIR}/${name}"
|
||||
}
|
||||
|
||||
link_cli "$DEPLOY/nx" nx
|
||||
link_cli "$DEPLOY/update.sh" nexus-update
|
||||
link_cli "$DEPLOY/install-nexus-fresh.sh" nexus-fresh
|
||||
link_cli "$DEPLOY/quick-install.sh" nexus-install
|
||||
link_cli "$DEPLOY/nexus-1panel.sh" nexus-1panel
|
||||
link_cli "$DEPLOY/install-1panel-docker.sh" nexus-1panel-docker
|
||||
|
||||
if command -v nx >/dev/null 2>&1; then
|
||||
info "全局命令已就绪: nx → $(readlink -f "$(command -v nx)" 2>/dev/null || command -v nx)"
|
||||
else
|
||||
error "nx 未出现在 PATH 中,请确认 $BIN_DIR 在 PATH 内"
|
||||
exit 1
|
||||
fi
|
||||
+1
-1
@@ -416,7 +416,7 @@ server {
|
||||
}
|
||||
|
||||
# Upload limit
|
||||
client_max_body_size 100m;
|
||||
client_max_body_size 500m;
|
||||
|
||||
# Well-known for SSL cert validation
|
||||
location ~ \\.well-known {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — 安装/更新 host crontab:health_monitor + db_backup
|
||||
#
|
||||
# sudo bash deploy/install_ops_cron.sh
|
||||
# NEXUS_ROOT=/opt/nexus sudo bash deploy/install_ops_cron.sh
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
DEPLOY_DIR="${NEXUS_ROOT}/deploy"
|
||||
HEALTH_MARKER="nexus-health-monitor"
|
||||
BACKUP_MARKER="nexus-mysql-backup"
|
||||
HEALTH_LOG="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||
BACKUP_LOG="${NEXUS_BACKUP_LOG:-/var/log/nexus_backup.log}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 运行: sudo bash $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_crontab() {
|
||||
if command -v crontab >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
warn "未检测到 crontab,尝试安装 cron 包…"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq cron
|
||||
systemctl enable --now cron 2>/dev/null || systemctl enable --now crond 2>/dev/null || true
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y cronie
|
||||
systemctl enable --now crond 2>/dev/null || true
|
||||
fi
|
||||
if ! command -v crontab >/dev/null 2>&1; then
|
||||
error "无法安装 crontab(请手动: apt install cron)"
|
||||
exit 1
|
||||
fi
|
||||
info "cron 已安装"
|
||||
}
|
||||
|
||||
ensure_crontab
|
||||
|
||||
for f in \
|
||||
"${DEPLOY_DIR}/health_monitor.sh" \
|
||||
"${DEPLOY_DIR}/db_backup.sh" \
|
||||
"${DEPLOY_DIR}/mysql_dump_to_file.sh" \
|
||||
; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
error "缺少脚本: $f"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$f" 2>/dev/null || true
|
||||
done
|
||||
|
||||
touch "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
|
||||
chmod 644 "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
|
||||
|
||||
HEALTH_LINE="* * * * * NEXUS_DEPLOY_DIR=${NEXUS_ROOT} bash ${DEPLOY_DIR}/health_monitor.sh >> ${HEALTH_LOG} 2>&1 # ${HEALTH_MARKER}"
|
||||
BACKUP_LINE="0 3 * * * NEXUS_ROOT=${NEXUS_ROOT} bash ${DEPLOY_DIR}/db_backup.sh >> ${BACKUP_LOG} 2>&1 # ${BACKUP_MARKER}"
|
||||
|
||||
existing="$(crontab -l 2>/dev/null || true)"
|
||||
filtered="$(printf '%s\n' "$existing" | grep -v "$HEALTH_MARKER" | grep -v "$BACKUP_MARKER" | grep -v 'deploy/health_monitor.sh' | grep -v 'deploy/db_backup.sh' || true)"
|
||||
{
|
||||
printf '%s\n' "$filtered" | sed '/^[[:space:]]*$/d'
|
||||
echo "$HEALTH_LINE"
|
||||
echo "$BACKUP_LINE"
|
||||
} | crontab -
|
||||
|
||||
info "已写入 crontab:"
|
||||
crontab -l | grep -E "$HEALTH_MARKER|$BACKUP_MARKER" || true
|
||||
info "健康巡检日志: $HEALTH_LOG"
|
||||
info "MySQL 备份日志: $BACKUP_LOG"
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — MySQL dump(Docker 1Panel / 宿主机 mysqldump 通用)
|
||||
#
|
||||
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql [--root /opt/nexus]
|
||||
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql --db-url 'mysql+aiomysql://...'
|
||||
#
|
||||
# 策略(按序):
|
||||
# 1. DB host 为运行中容器名 → docker exec 该容器内 mysqldump(连 127.0.0.1)
|
||||
# 2. 宿主机有 mysqldump 且 host 可解析 → 本机 mysqldump
|
||||
# 3. 1panel-network 存在 → docker run --rm mysql:8 客户端
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
OUTPUT=""
|
||||
DB_URL=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: mysql_dump_to_file.sh --output FILE.sql [--root /opt/nexus] [--db-url URL]
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output|-o) OUTPUT="$2"; shift 2 ;;
|
||||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||
--db-url) DB_URL="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "未知参数: $1" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
echo "缺少 --output" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
resolve_db_url() {
|
||||
if [[ -n "$DB_URL" ]]; then
|
||||
echo "$DB_URL"
|
||||
return 0
|
||||
fi
|
||||
local cname url
|
||||
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -n "$cname" ]]; then
|
||||
url="$(docker exec "$cname" grep -E '^NEXUS_DATABASE_URL=' /app/.env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
||||
[[ -n "$url" ]] && echo "$url" && return 0
|
||||
fi
|
||||
for f in \
|
||||
"${NEXUS_ROOT}/docker/.env.prod" \
|
||||
"${NEXUS_ROOT}/.env" \
|
||||
; do
|
||||
[[ -f "$f" ]] || continue
|
||||
url="$(grep -E '^NEXUS_DATABASE_URL=' "$f" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
||||
[[ -n "$url" ]] && echo "$url" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
DB_URL="$(resolve_db_url || true)"
|
||||
if [[ -z "$DB_URL" ]]; then
|
||||
echo "未找到 NEXUS_DATABASE_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$DB_URL" "$OUTPUT" <<'PY'
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
|
||||
def docker_names() -> set[str]:
|
||||
r = subprocess.run(
|
||||
["docker", "ps", "--format", "{{.Names}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return {ln.strip() for ln in (r.stdout or "").splitlines() if ln.strip()}
|
||||
|
||||
|
||||
def network_exists(name: str) -> bool:
|
||||
return (
|
||||
subprocess.run(
|
||||
["docker", "network", "inspect", name],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
).returncode
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
def write_dump(cmd: list[str], env: dict[str, str], out: str) -> int:
|
||||
r = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
sys.stderr.write(r.stderr or r.stdout or "mysqldump failed\n")
|
||||
return r.returncode
|
||||
with open(out, "w", encoding="utf-8") as fh:
|
||||
fh.write(r.stdout)
|
||||
return 0
|
||||
|
||||
|
||||
def dump_flags(db: str) -> list[str]:
|
||||
return [
|
||||
"--single-transaction",
|
||||
"--routines",
|
||||
"--triggers",
|
||||
"--set-gtid-purged=OFF",
|
||||
db,
|
||||
]
|
||||
|
||||
|
||||
url = sys.argv[1].replace("mysql+aiomysql://", "mysql://", 1)
|
||||
url = url.replace("mysql+asyncmy://", "mysql://", 1)
|
||||
out = sys.argv[2]
|
||||
p = urllib.parse.urlparse(url)
|
||||
user = urllib.parse.unquote(p.username or "")
|
||||
password = urllib.parse.unquote(p.password or "")
|
||||
host = p.hostname or "127.0.0.1"
|
||||
if host == "host.docker.internal":
|
||||
host = "127.0.0.1"
|
||||
port = str(p.port or 3306)
|
||||
db = (p.path or "/nexus").lstrip("/").split("?")[0]
|
||||
env = {**__import__("os").environ, "MYSQL_PWD": password}
|
||||
names = docker_names()
|
||||
|
||||
# 1) MySQL 跑在 Docker 容器内(1Panel 常见:URL host = 容器名)
|
||||
if host in names:
|
||||
cmd = [
|
||||
"docker",
|
||||
"exec",
|
||||
"-e",
|
||||
f"MYSQL_PWD={password}",
|
||||
host,
|
||||
"mysqldump",
|
||||
"-h",
|
||||
"127.0.0.1",
|
||||
"-P",
|
||||
port,
|
||||
"-u",
|
||||
user,
|
||||
*dump_flags(db),
|
||||
]
|
||||
rc = write_dump(cmd, env, out)
|
||||
if rc == 0:
|
||||
sys.exit(0)
|
||||
sys.stderr.write(f"docker exec {host} mysqldump 失败,尝试其它方式…\n")
|
||||
|
||||
# 2) 宿主机 mysqldump
|
||||
if shutil.which("mysqldump"):
|
||||
cmd = [
|
||||
"mysqldump",
|
||||
"-h",
|
||||
host,
|
||||
"-P",
|
||||
port,
|
||||
"-u",
|
||||
user,
|
||||
*dump_flags(db),
|
||||
]
|
||||
rc = write_dump(cmd, env, out)
|
||||
if rc == 0:
|
||||
sys.exit(0)
|
||||
|
||||
# 3) 临时 mysql 客户端容器(1panel-network)
|
||||
for net in ("1panel-network", "bridge"):
|
||||
if not network_exists(net):
|
||||
continue
|
||||
cmd = [
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--network",
|
||||
net,
|
||||
"-e",
|
||||
f"MYSQL_PWD={password}",
|
||||
"mysql:8.0",
|
||||
"mysqldump",
|
||||
"-h",
|
||||
host,
|
||||
"-P",
|
||||
port,
|
||||
"-u",
|
||||
user,
|
||||
*dump_flags(db),
|
||||
]
|
||||
rc = write_dump(cmd, env, out)
|
||||
if rc == 0:
|
||||
sys.exit(0)
|
||||
|
||||
sys.stderr.write("所有 mysqldump 策略均失败(无宿主机客户端且 Docker 回退不可用)\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
@@ -1,14 +1,22 @@
|
||||
# 可选 — 仅私人 Gitea 仓库需要(公共仓库匿名 clone,新机无需此文件)
|
||||
# 复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh
|
||||
|
||||
# NEXUS_GITEA_TOKEN='你的Gitea_Access_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'
|
||||
# 复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh
|
||||
# 用于 git push(scripts/git-push.sh);clone/pull 仍可匿名
|
||||
|
||||
# NEXUS_GITEA_HOST='axs.kuma1xn.vip' # HTTPS Gitea
|
||||
# NEXUS_GITEA_SCHEME='https'
|
||||
# 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
+563
-48
@@ -2,7 +2,7 @@
|
||||
# Nexus 6.0 — 1Panel + Docker 一键安装 / 升级(参数已全部内置,无需 export)
|
||||
#
|
||||
# 新机 1Panel 终端一条命令:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/nexus-1panel.sh" | bash -s install
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/nexus-1panel.sh" | bash -s install
|
||||
#
|
||||
# 已 clone 到 /opt/nexus:
|
||||
# cd /opt/nexus && bash deploy/nexus-1panel.sh install --skip-clone
|
||||
@@ -23,7 +23,8 @@ set -euo pipefail
|
||||
# 内置路径/域名(无敏感信息,可提交 Git)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
|
||||
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
@@ -34,6 +35,7 @@ NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
|
||||
COMPOSE_FILE="docker/docker-compose.prod.yml"
|
||||
COMPOSE_1PANEL="docker/docker-compose.1panel.yml"
|
||||
ENV_PROD="docker/.env.prod"
|
||||
HOST_PROFILE_FILE="docker/.host-profile"
|
||||
|
||||
@@ -49,11 +51,14 @@ error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
SKIP_CLONE=false
|
||||
SKIP_GIT=false
|
||||
FRESH_INSTALL=false
|
||||
REUSE_SECRETS=false
|
||||
FROM_ENV="${NEXUS_FROM_ENV}"
|
||||
NO_BACKUP=false
|
||||
REQUIRE_BACKUP=false
|
||||
PRUNE_IMAGES=false
|
||||
NO_CACHE=false
|
||||
CHECK_ONLY=false
|
||||
DRY_RUN=false
|
||||
CUSTOM_CPUS=""
|
||||
@@ -81,7 +86,7 @@ Nexus 1Panel 一键脚本
|
||||
--domain NAME 覆盖域名
|
||||
|
||||
升级选项:
|
||||
--no-backup --prune --dry-run
|
||||
--no-backup --require-backup --prune --no-cache --dry-run --check(仅查 Git,不重建)
|
||||
|
||||
Git: 公共仓库匿名 clone(无需令牌);私人仓库见 init-token
|
||||
EOF
|
||||
@@ -178,13 +183,350 @@ apply_pool_to_env_prod() {
|
||||
local root="$1" prof="$2"
|
||||
local env_file="$root/$ENV_PROD"
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
if [[ ! -f "$prof" ]]; then
|
||||
warn "未找到资源档位文件: $prof,保留 $ENV_PROD 中现有数据库连接池配置"
|
||||
return 0
|
||||
fi
|
||||
local pool overflow
|
||||
pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2-)"
|
||||
overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2-)"
|
||||
pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2- || true)"
|
||||
overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2- || true)"
|
||||
[[ -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"
|
||||
}
|
||||
|
||||
has_1panel_network() {
|
||||
docker network inspect 1panel-network >/dev/null 2>&1 \
|
||||
|| sudo docker network inspect 1panel-network >/dev/null 2>&1
|
||||
}
|
||||
|
||||
upsert_env_var() {
|
||||
local file="$1" key="$2" val="$3"
|
||||
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
||||
sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
||||
else
|
||||
echo "${key}=${val}" >>"$file"
|
||||
fi
|
||||
}
|
||||
|
||||
migrate_redis_url_host() {
|
||||
local old_url="$1" new_host="$2"
|
||||
python3 - "$old_url" "$new_host" <<'PY'
|
||||
import sys
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
|
||||
old_url, new_host = sys.argv[1], sys.argv[2]
|
||||
u = urlparse(old_url)
|
||||
if u.scheme != "redis":
|
||||
sys.exit(0)
|
||||
password = unquote(u.password or "")
|
||||
if not password and u.username:
|
||||
password = unquote(u.username)
|
||||
if password and new_host:
|
||||
print(f"redis://:{quote(password, safe='')}@{new_host}:6379/0")
|
||||
PY
|
||||
}
|
||||
|
||||
# Prefer wizard-written redis://:pass@host from nexus-state or .env.prod; avoid clobbering on nx update.
|
||||
resolve_nexus_redis_url() {
|
||||
local redis_host="$1"
|
||||
local env_file="$2"
|
||||
local vol_url="" existing="" migrated="" host_lc="${redis_host,,}"
|
||||
|
||||
local state_vol
|
||||
state_vol="$(docker volume ls -q --filter name=nexus-state 2>/dev/null | head -1 || true)"
|
||||
if [[ -n "$state_vol" ]]; then
|
||||
vol_url="$(docker run --rm -v "${state_vol}:/data" alpine sh -c \
|
||||
"grep '^NEXUS_REDIS_URL=' /data/.env 2>/dev/null | cut -d= -f2- | tr -d '\"'" 2>/dev/null || true)"
|
||||
if [[ -n "$vol_url" ]]; then
|
||||
if [[ "${vol_url,,}" == *"${host_lc}"* ]]; then
|
||||
echo "$vol_url"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$vol_url" == *"@"* ]]; then
|
||||
migrated="$(migrate_redis_url_host "$vol_url" "$redis_host" 2>/dev/null || true)"
|
||||
if [[ -n "$migrated" ]]; then
|
||||
echo "$migrated"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$env_file" ]]; then
|
||||
existing="$(grep '^NEXUS_REDIS_URL=' "$env_file" 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
|
||||
if [[ -n "$existing" && "$existing" == *"@"* ]]; then
|
||||
if [[ "${existing,,}" == *"${host_lc}"* ]]; then
|
||||
echo "$existing"
|
||||
return 0
|
||||
fi
|
||||
migrated="$(migrate_redis_url_host "$existing" "$redis_host" 2>/dev/null || true)"
|
||||
if [[ -n "$migrated" ]]; then
|
||||
echo "$migrated"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "redis://${redis_host}:6379/0"
|
||||
}
|
||||
|
||||
nexus_container_name() {
|
||||
docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||||
}
|
||||
|
||||
nexus_publish_port() {
|
||||
local root="$1"
|
||||
local env_file="$root/$ENV_PROD"
|
||||
local port
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$env_file" 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
|
||||
echo "${port:-8600}"
|
||||
}
|
||||
|
||||
nexus_container_has_install_lock() {
|
||||
local cname="$1"
|
||||
docker exec "$cname" test -f /app/.install_locked 2>/dev/null \
|
||||
|| docker exec "$cname" test -f /var/lib/nexus/.install_locked 2>/dev/null
|
||||
}
|
||||
|
||||
restore_install_lock_in_container() {
|
||||
local cname="$1"
|
||||
[[ -n "$cname" ]] || return 0
|
||||
docker exec "$cname" sh -c '
|
||||
if [ ! -f /app/.install_locked ] && [ -f /var/lib/nexus/.install_locked ]; then
|
||||
cp /var/lib/nexus/.install_locked /app/.install_locked
|
||||
chmod 600 /app/.install_locked
|
||||
fi
|
||||
' 2>/dev/null || true
|
||||
}
|
||||
|
||||
nexus_on_1panel_network() {
|
||||
local cname="$1"
|
||||
[[ -n "$cname" ]] || return 1
|
||||
docker inspect "$cname" --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' 2>/dev/null \
|
||||
| grep -qw '1panel-network'
|
||||
}
|
||||
|
||||
verify_nexus_1panel_network() {
|
||||
local root="$1"
|
||||
if ! has_1panel_network; then
|
||||
return 0
|
||||
fi
|
||||
local cname
|
||||
cname="$(nexus_container_name)"
|
||||
[[ -n "$cname" ]] || return 0
|
||||
restore_install_lock_in_container "$cname"
|
||||
if nexus_on_1panel_network "$cname"; then
|
||||
info "已验证 Nexus 接入 1panel-network"
|
||||
return 0
|
||||
fi
|
||||
error "Nexus 容器 ${cname} 未接入 1panel-network,无法解析 1Panel MySQL/Redis 主机名"
|
||||
warn "正在叠加 docker-compose.1panel.yml 重新创建容器..."
|
||||
compose_cmd "$root" up -d --no-build --remove-orphans
|
||||
sleep 2
|
||||
cname="$(nexus_container_name)"
|
||||
restore_install_lock_in_container "$cname"
|
||||
if nexus_on_1panel_network "$cname"; then
|
||||
info "已修复:Nexus 已接入 1panel-network"
|
||||
return 0
|
||||
fi
|
||||
error "修复失败:Nexus 仍未接入 1panel-network"
|
||||
return 1
|
||||
}
|
||||
|
||||
set_install_complete() {
|
||||
local root="$1"
|
||||
local env_file="$root/$ENV_PROD"
|
||||
local cname
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
cname="$(nexus_container_name)"
|
||||
[[ -n "$cname" ]] || return 0
|
||||
restore_install_lock_in_container "$cname"
|
||||
if nexus_container_has_install_lock "$cname"; then
|
||||
archive_install_wizard_in_container "$cname"
|
||||
upsert_env_var "$env_file" "NEXUS_INSTALL_WIZARD_PENDING" "0"
|
||||
info "安装已完成:NEXUS_INSTALL_WIZARD_PENDING=0"
|
||||
fi
|
||||
}
|
||||
|
||||
sync_env_prod_to_volume() {
|
||||
local root="$1"
|
||||
local env_file="$root/$ENV_PROD"
|
||||
local state_vol cname
|
||||
state_vol="$(docker volume ls -q --filter name=nexus-state 2>/dev/null | head -1 || true)"
|
||||
[[ -n "$state_vol" && -f "$env_file" ]] || return 0
|
||||
cname="$(nexus_container_name)"
|
||||
[[ -n "$cname" ]] || return 0
|
||||
restore_install_lock_in_container "$cname"
|
||||
nexus_container_has_install_lock "$cname" || return 0
|
||||
python3 - "$env_file" "$state_vol" <<'PY'
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
env_file = Path(sys.argv[1])
|
||||
vol = sys.argv[2]
|
||||
keys = (
|
||||
"NEXUS_SECRET_KEY",
|
||||
"NEXUS_API_KEY",
|
||||
"NEXUS_ENCRYPTION_KEY",
|
||||
"NEXUS_REDIS_URL",
|
||||
"NEXUS_DATABASE_URL",
|
||||
"NEXUS_TELEGRAM_BOT_TOKEN",
|
||||
"NEXUS_TELEGRAM_CHAT_ID",
|
||||
)
|
||||
prod = {}
|
||||
for line in env_file.read_text(encoding="utf-8").splitlines():
|
||||
m = re.match(r"^([A-Z0-9_]+)=(.*)$", line.strip())
|
||||
if m and m.group(1) in keys and m.group(2).strip():
|
||||
prod[m.group(1)] = m.group(2).strip().strip('"')
|
||||
if not prod:
|
||||
sys.exit(0)
|
||||
cur = subprocess.run(
|
||||
["docker", "run", "--rm", "-v", f"{vol}:/data", "alpine", "cat", "/data/.env"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
text = cur.stdout if cur.returncode == 0 else ""
|
||||
if not text.strip():
|
||||
sys.exit(0)
|
||||
lines = text.splitlines()
|
||||
out = []
|
||||
seen = set()
|
||||
for line in lines:
|
||||
key = line.split("=", 1)[0].strip() if "=" in line else ""
|
||||
if key in prod:
|
||||
out.append(f'{key}={prod[key]}')
|
||||
seen.add(key)
|
||||
else:
|
||||
out.append(line)
|
||||
for key, val in prod.items():
|
||||
if key not in seen:
|
||||
out.append(f'{key}={val}')
|
||||
new_text = "\n".join(out).rstrip() + "\n"
|
||||
subprocess.run(
|
||||
["docker", "run", "--rm", "-i", "-v", f"{vol}:/data", "alpine", "sh", "-c", "cat > /data/.env && chmod 600 /data/.env"],
|
||||
input=new_text,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
print("synced")
|
||||
PY
|
||||
info "已将 docker/.env.prod 密钥/URL 同步至 nexus-state 卷"
|
||||
}
|
||||
|
||||
tag_rollback_image() {
|
||||
local root="$1" old_id
|
||||
old_id="$(docker ps --filter "name=nexus-prod-nexus" --format '{{.Image}}' 2>/dev/null | head -1 || true)"
|
||||
if [[ -z "$old_id" ]]; then
|
||||
old_id="$(compose_cmd "$root" images -q nexus 2>/dev/null | head -1 || true)"
|
||||
fi
|
||||
if [[ -n "$old_id" ]]; then
|
||||
docker tag "$old_id" "nexus-prod-nexus:rollback" 2>/dev/null || true
|
||||
info "已标记回滚镜像 nexus-prod-nexus:rollback"
|
||||
fi
|
||||
}
|
||||
|
||||
gate_new_image_import() {
|
||||
local root="$1" img
|
||||
img="$(compose_cmd "$root" images -q nexus 2>/dev/null | head -1 || true)"
|
||||
if [[ -z "$img" ]]; then
|
||||
error "镜像门控失败:未找到 nexus 镜像"
|
||||
return 1
|
||||
fi
|
||||
if ! docker run --rm --entrypoint python3 "$img" -c "import server.main" >/dev/null 2>&1; then
|
||||
error "镜像门控失败:import server.main"
|
||||
return 1
|
||||
fi
|
||||
info "镜像门控通过 (import server.main)"
|
||||
return 0
|
||||
}
|
||||
|
||||
rollback_compose_upgrade() {
|
||||
local root="$1"
|
||||
if docker image inspect nexus-prod-nexus:rollback >/dev/null 2>&1; then
|
||||
warn "健康检查失败,回滚至上一镜像..."
|
||||
docker tag nexus-prod-nexus:rollback nexus-prod-nexus:latest 2>/dev/null || true
|
||||
compose_cmd "$root" up -d --no-build --remove-orphans
|
||||
if ! verify_nexus_1panel_network "$root"; then
|
||||
error "回滚后仍未接入 1panel-network"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
warn "无回滚镜像 nexus-prod-nexus:rollback,跳过自动回滚"
|
||||
return 1
|
||||
}
|
||||
|
||||
write_1panel_hosts_volume() {
|
||||
local mysql_host="${1:-}" redis_host="${2:-}"
|
||||
[[ -n "$mysql_host" || -n "$redis_host" ]] || return 0
|
||||
local vol json
|
||||
vol="$(docker volume ls -q --filter name=nexus-web-data 2>/dev/null | head -1 || true)"
|
||||
if [[ -z "$vol" ]]; then
|
||||
warn "未找到 nexus-web-data 卷,跳过写入 1panel-hosts.json"
|
||||
return 0
|
||||
fi
|
||||
json="{"
|
||||
if [[ -n "$mysql_host" ]]; then
|
||||
json+="\"db_host\":\"${mysql_host}\""
|
||||
[[ -n "$redis_host" ]] && json+=","
|
||||
fi
|
||||
if [[ -n "$redis_host" ]]; then
|
||||
json+="\"redis_host\":\"${redis_host}\""
|
||||
fi
|
||||
json+="}"
|
||||
if printf '%s' "$json" | docker run --rm -i -v "${vol}:/data" alpine:3.19 \
|
||||
sh -c 'cat > /data/1panel-hosts.json && chmod 644 /data/1panel-hosts.json'; then
|
||||
info "已写入卷 ${vol} → web/data/1panel-hosts.json"
|
||||
else
|
||||
warn "写入 1panel-hosts.json 失败(向导仍可读 .env.prod 环境变量)"
|
||||
fi
|
||||
}
|
||||
|
||||
sync_1panel_service_hosts() {
|
||||
local root="$1"
|
||||
local env_file="$root/$ENV_PROD"
|
||||
local detect="$root/deploy/detect-1panel-services.sh"
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
if ! has_1panel_network; then
|
||||
warn "未找到 1panel-network — 请先在 1Panel 应用商店安装 MySQL/Redis"
|
||||
return 0
|
||||
fi
|
||||
if [[ ! -x "$detect" ]]; then
|
||||
chmod +x "$detect" 2>/dev/null || true
|
||||
fi
|
||||
if [[ ! -x "$detect" ]]; then
|
||||
warn "未找到 detect-1panel-services.sh,跳过 1Panel 容器名探测"
|
||||
return 0
|
||||
fi
|
||||
local line mysql_host="" redis_host="" detect_out
|
||||
if ! detect_out="$("$detect" 2>&1)"; then
|
||||
warn "1Panel 容器探测: ${detect_out:-无 MySQL/Redis 容器运行}"
|
||||
return 0
|
||||
fi
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
NEXUS_1PANEL_DB_HOST=*)
|
||||
mysql_host="${line#NEXUS_1PANEL_DB_HOST=}"
|
||||
upsert_env_var "$env_file" NEXUS_1PANEL_DB_HOST "$mysql_host"
|
||||
info "1Panel MySQL 容器: $mysql_host(安装向导将预填为数据库主机)"
|
||||
;;
|
||||
NEXUS_1PANEL_REDIS_HOST=*)
|
||||
redis_host="${line#NEXUS_1PANEL_REDIS_HOST=}"
|
||||
upsert_env_var "$env_file" NEXUS_1PANEL_REDIS_HOST "$redis_host"
|
||||
local redis_url
|
||||
redis_url="$(resolve_nexus_redis_url "$redis_host" "$env_file")"
|
||||
upsert_env_var "$env_file" "NEXUS_REDIS_URL" "$redis_url"
|
||||
if [[ "$redis_url" == *"@"* ]]; then
|
||||
info "1Panel Redis 容器: $redis_host(NEXUS_REDIS_URL 已保留向导密码)"
|
||||
else
|
||||
info "1Panel Redis 容器: $redis_host(NEXUS_REDIS_URL 占位,向导步骤 3 填密码后写入卷 .env)"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done <<<"$detect_out"
|
||||
write_1panel_hosts_volume "$mysql_host" "$redis_host"
|
||||
}
|
||||
|
||||
compose_cmd() {
|
||||
local root="$1"
|
||||
shift
|
||||
@@ -195,7 +537,11 @@ compose_cmd() {
|
||||
exit 1
|
||||
fi
|
||||
cd "$root"
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_PROD" --env-file "$prof" "$@"
|
||||
local -a compose_args=(-f "$COMPOSE_FILE")
|
||||
if has_1panel_network && [[ -f "$COMPOSE_1PANEL" ]]; then
|
||||
compose_args+=(-f "$COMPOSE_1PANEL")
|
||||
fi
|
||||
docker compose "${compose_args[@]}" --env-file "$ENV_PROD" --env-file "$prof" "$@"
|
||||
}
|
||||
|
||||
rand_secret() {
|
||||
@@ -216,7 +562,11 @@ parse_gitea_endpoint() {
|
||||
GITEA_HOST="${hp%%:*}"
|
||||
GITEA_PORT="${hp##*:}"
|
||||
if [[ "$GITEA_HOST" == "$GITEA_PORT" || -z "$GITEA_PORT" ]]; then
|
||||
GITEA_PORT=3000
|
||||
case "${NEXUS_GITEA_SCHEME:-https}" in
|
||||
http) GITEA_PORT=80 ;;
|
||||
https) GITEA_PORT=443 ;;
|
||||
*) GITEA_PORT=443 ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -385,10 +735,11 @@ require_token() {
|
||||
}
|
||||
|
||||
git_remote_url() {
|
||||
local scheme="${NEXUS_GITEA_SCHEME:-https}"
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
echo "${scheme}://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
else
|
||||
echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
echo "${scheme}://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -449,8 +800,6 @@ write_temp_secrets_env() {
|
||||
local f
|
||||
f="$(mktemp /tmp/nexus-secrets-import.XXXXXX.env)"
|
||||
{
|
||||
printf 'MYSQL_ROOT_PASSWORD=%s\n' "${MYSQL_ROOT_PASSWORD:-$(rand_pass)}"
|
||||
printf 'MYSQL_PASSWORD=%s\n' "${MYSQL_PASSWORD:-$(rand_pass)}"
|
||||
printf 'NEXUS_SECRET_KEY=%s\n' "$NEXUS_SECRET_KEY"
|
||||
printf 'NEXUS_API_KEY=%s\n' "$NEXUS_API_KEY"
|
||||
printf 'NEXUS_ENCRYPTION_KEY=%s\n' "$NEXUS_ENCRYPTION_KEY"
|
||||
@@ -497,7 +846,7 @@ write_env_prod() {
|
||||
--from-env "$FROM_ENV" \
|
||||
--backup "$backup"
|
||||
elif [[ "$FRESH_INSTALL" == true ]]; then
|
||||
info "全新安装:为本机生成唯一 MYSQL / NEXUS 密钥..."
|
||||
info "全新安装:为本机生成唯一 NEXUS 密钥(MySQL/Redis 请自行安装)..."
|
||||
python3 "$gen" write-prod \
|
||||
--template "$example" \
|
||||
--output "$env_file" \
|
||||
@@ -539,26 +888,96 @@ write_env_prod() {
|
||||
info "已写入 $env_file"
|
||||
}
|
||||
|
||||
compose_up() {
|
||||
purge_legacy_bundled_services() {
|
||||
local root="$1"
|
||||
local s
|
||||
for s in \
|
||||
"$root/deploy/uninstall-mysql-compose.sh" \
|
||||
"$root/deploy/uninstall-redis-compose.sh"; do
|
||||
if [[ -x "$s" ]]; then
|
||||
step "清理遗留 Compose 内置服务: $(basename "$s")"
|
||||
NEXUS_ROOT="$root" bash "$s" || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
reset_nexus_state_volume_for_fresh() {
|
||||
local vol
|
||||
vol="$(docker volume ls -q --filter name=nexus-state 2>/dev/null | head -1 || true)"
|
||||
[[ -n "$vol" ]] || return 0
|
||||
warn "全新安装:清除 nexus-state 卷内旧 .env / .install_locked"
|
||||
docker run --rm -v "${vol}:/data" alpine:3.19 \
|
||||
sh -c 'rm -f /data/.env /data/.install_locked' 2>/dev/null || true
|
||||
}
|
||||
|
||||
restore_install_wizard_html_in_container() {
|
||||
local cname="$1"
|
||||
[[ -n "$cname" ]] || return 0
|
||||
docker exec "$cname" sh -c '
|
||||
rm -f /app/.install_locked
|
||||
if [ -f /app/web/app/install.html.bak ] && [ ! -f /app/web/app/install.html ]; then
|
||||
mv /app/web/app/install.html.bak /app/web/app/install.html
|
||||
fi
|
||||
' 2>/dev/null || true
|
||||
}
|
||||
|
||||
compose_up() {
|
||||
local root="$1" cname
|
||||
save_host_profile "$root"
|
||||
info "档位: ${NEXUS_PROFILE}(MySQL/Nexus 内存与连接池已套用;Redis 请自行安装)"
|
||||
purge_legacy_bundled_services "$root"
|
||||
sync_1panel_service_hosts "$root"
|
||||
if [[ "$FRESH_INSTALL" == true ]]; then
|
||||
reset_nexus_state_volume_for_fresh
|
||||
fi
|
||||
if has_1panel_network; then
|
||||
info "已接入 1panel-network(MySQL/Redis 使用 1Panel 容器名互联)"
|
||||
fi
|
||||
info "档位: ${NEXUS_PROFILE}(仅 Nexus 容器;MySQL/Redis 请自行安装)"
|
||||
info "构建并启动(首次约 10~20 分钟)..."
|
||||
compose_cmd "$root" up -d --build
|
||||
compose_cmd "$root" up -d --build --remove-orphans
|
||||
verify_nexus_1panel_network "$root" || return 1
|
||||
if [[ "$FRESH_INSTALL" == true ]]; then
|
||||
cname="$(nexus_container_name)"
|
||||
restore_install_wizard_html_in_container "$cname"
|
||||
fi
|
||||
}
|
||||
|
||||
archive_install_wizard_in_container() {
|
||||
local cname="$1"
|
||||
[[ -n "$cname" ]] || return 0
|
||||
docker exec "$cname" sh -c '
|
||||
if [ -f /app/web/app/install.html.bak ]; then
|
||||
rm -f /app/web/app/install.html
|
||||
elif [ -f /app/web/app/install.html ]; then
|
||||
mv /app/web/app/install.html /app/web/app/install.html.bak
|
||||
fi
|
||||
' 2>/dev/null || true
|
||||
}
|
||||
|
||||
verify_install_wizard() {
|
||||
local root="$1"
|
||||
local code
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8600/app/install.html" 2>/dev/null || echo "000")
|
||||
local port="${2:-$(nexus_publish_port "$root")}"
|
||||
local code cname
|
||||
cname="$(nexus_container_name)"
|
||||
if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
|
||||
archive_install_wizard_in_container "$cname"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||||
if [[ "$code" == "404" ]]; then
|
||||
info " /app/install.html 已归档 → HTTP 404"
|
||||
return 0
|
||||
fi
|
||||
warn " 已锁定但 /app/install.html → HTTP $code"
|
||||
return 1
|
||||
fi
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||||
if [[ "$code" == "200" ]]; then
|
||||
info " /app/install.html → HTTP 200"
|
||||
return 0
|
||||
fi
|
||||
warn " /app/install.html → HTTP $code(尝试同步静态文件…)"
|
||||
if [[ -x "$root/deploy/sync-install-wizard-to-container.sh" ]]; then
|
||||
NEXUS_ROOT="$root" bash "$root/deploy/sync-install-wizard-to-container.sh" || true
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8600/app/install.html" 2>/dev/null || echo "000")
|
||||
NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$root" bash "$root/deploy/sync-install-wizard-to-container.sh" || true
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||||
if [[ "$code" == "200" ]]; then
|
||||
info " /app/install.html → HTTP 200(已同步)"
|
||||
return 0
|
||||
@@ -570,13 +989,14 @@ verify_install_wizard() {
|
||||
|
||||
verify_health() {
|
||||
local root="$1" title="${2:-安装}"
|
||||
local i health spa
|
||||
info "等待服务就绪..."
|
||||
local i health spa port
|
||||
port="$(nexus_publish_port "$root")"
|
||||
info "等待服务就绪 (端口 ${port})..."
|
||||
for i in $(seq 1 60); do
|
||||
health=$(curl -sf "http://127.0.0.1:8600/health" 2>/dev/null || true)
|
||||
health=$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || true)
|
||||
if [[ "$health" == "ok" ]]; then
|
||||
spa=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8600/app/" 2>/dev/null || echo "000")
|
||||
verify_install_wizard "$root" || true
|
||||
spa=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo "000")
|
||||
verify_install_wizard "$root" "$port" || true
|
||||
echo ""
|
||||
info "═══ ${title}完成 ═══"
|
||||
info " 档位: ${NEXUS_PROFILE}"
|
||||
@@ -587,7 +1007,7 @@ verify_health() {
|
||||
info " 版本: $(git -C "$root" log -1 --oneline)"
|
||||
fi
|
||||
echo ""
|
||||
info "1Panel → 网站 → 反代 http://127.0.0.1:8600 域名: ${NEXUS_DOMAIN}"
|
||||
info "1Panel → 网站 → 反代 http://127.0.0.1:${port} 域名: ${NEXUS_DOMAIN}"
|
||||
info "配置示例: $root/deploy/1panel/openresty-nexus.conf.example"
|
||||
if [[ "$FRESH_INSTALL" == true ]] || ! grep -qE '^NEXUS_SECRET_KEY=.+$' "$root/$ENV_PROD" 2>/dev/null; then
|
||||
info "浏览器: https://${NEXUS_DOMAIN}/app/install.html"
|
||||
@@ -595,8 +1015,8 @@ verify_health() {
|
||||
info "浏览器: https://${NEXUS_DOMAIN}/app/"
|
||||
fi
|
||||
echo ""
|
||||
info "运维菜单: nx 或 bash $root/deploy/nx god"
|
||||
info " (安装 nx 命令: ln -sf $root/deploy/nx /usr/local/bin/nx)"
|
||||
NEXUS_ROOT="$root" bash "$root/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||
info "运维菜单: nx 或 nexus-update"
|
||||
check_ports_post "$root"
|
||||
return 0
|
||||
fi
|
||||
@@ -605,14 +1025,18 @@ verify_health() {
|
||||
error "健康检查超时"
|
||||
check_ports_post "$root" || true
|
||||
compose_cmd "$root" logs --tail=80 nexus || true
|
||||
exit 1
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_repo() {
|
||||
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
|
||||
if [[ ! -d "$NEXUS_ROOT" ]]; then
|
||||
error "未找到 $NEXUS_ROOT,请先: bash $0 install"
|
||||
exit 1
|
||||
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
|
||||
error "缺少 $NEXUS_ROOT/$ENV_PROD"
|
||||
exit 1
|
||||
@@ -645,26 +1069,29 @@ show_pending() {
|
||||
}
|
||||
|
||||
backup_mysql() {
|
||||
local root="$1" ts file mysql_root
|
||||
local root="$1" ts file dump_sh
|
||||
if [[ "$NO_BACKUP" == true ]]; then
|
||||
warn "已跳过备份 (--no-backup)"
|
||||
return 0
|
||||
fi
|
||||
mysql_root="$(read_env_val "$root/$ENV_PROD" MYSQL_ROOT_PASSWORD)"
|
||||
if [[ -z "$mysql_root" ]]; then
|
||||
warn "无法读取 MYSQL_ROOT_PASSWORD,跳过备份"
|
||||
dump_sh="${root}/deploy/mysql_dump_to_file.sh"
|
||||
if [[ ! -f "$dump_sh" ]]; then
|
||||
warn "缺少 $dump_sh,跳过 MySQL 备份"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$NEXUS_BACKUP_DIR"
|
||||
ts="$(date +%Y%m%d-%H%M%S)"
|
||||
file="${NEXUS_BACKUP_DIR}/nexus-before-upgrade-${ts}.sql"
|
||||
step "备份 MySQL → $file"
|
||||
if ! compose_cmd "$root" exec -T mysql \
|
||||
mysqldump -uroot -p"${mysql_root}" --single-transaction --routines --triggers nexus \
|
||||
>"$file" 2>/dev/null; then
|
||||
error "mysqldump 失败,已中止"
|
||||
if ! NEXUS_ROOT="$root" bash "$dump_sh" --output "$file" --root "$root"; then
|
||||
if [[ "$REQUIRE_BACKUP" == true ]]; then
|
||||
error "mysqldump 失败,已中止(--require-backup)"
|
||||
rm -f "$file"
|
||||
exit 1
|
||||
fi
|
||||
warn "mysqldump 失败,继续升级(加 --require-backup 可在备份失败时中止)"
|
||||
rm -f "$file"
|
||||
exit 1
|
||||
return 0
|
||||
fi
|
||||
gzip -f "$file"
|
||||
info "备份: ${file}.gz"
|
||||
@@ -706,9 +1133,10 @@ cmd_install() {
|
||||
ensure_docker
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
clone_or_pull
|
||||
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||
write_env_prod "$NEXUS_ROOT"
|
||||
compose_up "$NEXUS_ROOT"
|
||||
verify_health "$NEXUS_ROOT" "安装"
|
||||
verify_health "$NEXUS_ROOT" "安装" || exit 1
|
||||
}
|
||||
|
||||
cmd_upgrade() {
|
||||
@@ -725,11 +1153,13 @@ cmd_upgrade() {
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
resolve_profile
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
git_fetch
|
||||
|
||||
if ! show_pending; then
|
||||
exit 0
|
||||
if [[ "$SKIP_GIT" == true ]]; then
|
||||
info "跳过 Git 同步(本机 rsync 已推送代码)"
|
||||
else
|
||||
git_fetch
|
||||
show_pending || true
|
||||
fi
|
||||
|
||||
if [[ "$CHECK_ONLY" == true ]]; then
|
||||
exit 0
|
||||
fi
|
||||
@@ -738,16 +1168,98 @@ cmd_upgrade() {
|
||||
exit 0
|
||||
fi
|
||||
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
git_upgrade
|
||||
if [[ "$SKIP_GIT" == true ]]; then
|
||||
info "本机直传模式:跳过 git reset,仍备份并重建镜像"
|
||||
if [[ "$NO_BACKUP" != true ]]; then
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
fi
|
||||
else
|
||||
local behind
|
||||
behind="$(commits_behind)"
|
||||
if [[ "$behind" != "0" ]]; then
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
git_upgrade
|
||||
else
|
||||
info "Git 已最新,仍重建 Docker 镜像与容器(避免旧 entrypoint 缓存)"
|
||||
if [[ "$NO_BACKUP" != true ]]; then
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")"
|
||||
upsert_env_var "$NEXUS_ROOT/$ENV_PROD" "NEXUS_HOST_ROOT" "$NEXUS_ROOT"
|
||||
save_host_profile "$NEXUS_ROOT"
|
||||
step "重建容器..."
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build
|
||||
purge_legacy_bundled_services "$NEXUS_ROOT"
|
||||
sync_1panel_service_hosts "$NEXUS_ROOT"
|
||||
sync_env_prod_to_volume "$NEXUS_ROOT"
|
||||
tag_rollback_image "$NEXUS_ROOT"
|
||||
if [[ "$NO_CACHE" == true ]]; then
|
||||
step "无缓存重建 Nexus 镜像..."
|
||||
compose_cmd "$NEXUS_ROOT" build --no-cache nexus
|
||||
else
|
||||
step "重建 Nexus 镜像..."
|
||||
compose_cmd "$NEXUS_ROOT" build nexus
|
||||
fi
|
||||
if ! gate_new_image_import "$NEXUS_ROOT"; then
|
||||
exit 1
|
||||
fi
|
||||
step "重建容器(--remove-orphans 清理旧 mysql/redis)..."
|
||||
compose_cmd "$NEXUS_ROOT" up -d --no-build --remove-orphans
|
||||
if ! verify_nexus_1panel_network "$NEXUS_ROOT"; then
|
||||
rollback_compose_upgrade "$NEXUS_ROOT" || true
|
||||
exit 1
|
||||
fi
|
||||
local port wiz_code i health
|
||||
port="$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
for i in $(seq 1 30); do
|
||||
health="$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
||||
[[ "$health" == "ok" ]] && break
|
||||
sleep 2
|
||||
done
|
||||
cname="$(nexus_container_name)"
|
||||
if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
|
||||
archive_install_wizard_in_container "$cname"
|
||||
info "安装已锁定,跳过 install.html 热同步"
|
||||
else
|
||||
wiz_code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")"
|
||||
if [[ "$wiz_code" != "200" && -x "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh" ]]; then
|
||||
NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh"
|
||||
fi
|
||||
fi
|
||||
set_install_complete "$NEXUS_ROOT"
|
||||
if [[ "$PRUNE_IMAGES" == true ]]; then
|
||||
docker image prune -f >/dev/null 2>&1 || true
|
||||
fi
|
||||
verify_health "$NEXUS_ROOT" "升级"
|
||||
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
|
||||
rollback_compose_upgrade "$NEXUS_ROOT" || true
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=./detect_deploy_runtime.sh
|
||||
if [[ -f "$NEXUS_ROOT/deploy/detect_deploy_runtime.sh" ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$NEXUS_ROOT/deploy/detect_deploy_runtime.sh"
|
||||
if [[ -t 0 ]]; then
|
||||
prompt_ops_cron_if_missing "$NEXUS_ROOT"
|
||||
else
|
||||
suggest_ops_cron "$NEXUS_ROOT"
|
||||
fi
|
||||
fi
|
||||
local vcname
|
||||
vcname="$(nexus_container_name)"
|
||||
if [[ -n "$vcname" ]] && nexus_container_has_install_lock "$vcname" \
|
||||
&& [[ -x "$NEXUS_ROOT/deploy/verify-1panel-install-wizard.sh" ]]; then
|
||||
step "升级后验收..."
|
||||
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/verify-1panel-install-wizard.sh" || \
|
||||
warn "验收脚本报告 FAIL — 见上方输出"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_check() {
|
||||
@@ -786,13 +1298,16 @@ parse_common_upgrade() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage ;;
|
||||
--skip-git) SKIP_GIT=true; shift ;;
|
||||
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
||||
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
|
||||
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
|
||||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||
--branch) GIT_BRANCH="$2"; shift 2 ;;
|
||||
--no-backup) NO_BACKUP=true; shift ;;
|
||||
--require-backup) REQUIRE_BACKUP=true; shift ;;
|
||||
--prune|--prune-images) PRUNE_IMAGES=true; shift ;;
|
||||
--no-cache|--rebuild) NO_CACHE=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--check) CHECK_ONLY=true; shift ;;
|
||||
*)
|
||||
|
||||
@@ -75,5 +75,5 @@ server {
|
||||
error_log {{LOG_DIR}}/{{SERVER_NAME}}_nexus.error.log;
|
||||
|
||||
# ── Upload limit ──
|
||||
client_max_body_size 100m;
|
||||
client_max_body_size 500m;
|
||||
}
|
||||
|
||||
@@ -109,5 +109,5 @@ server {
|
||||
error_log {{LOG_DIR}}/{{SERVER_NAME}}.error.log;
|
||||
|
||||
# ── Upload limit ──
|
||||
client_max_body_size 100m;
|
||||
client_max_body_size 500m;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus NX — 一键安装 + 上帝菜单(1Panel Docker 生产栈)
|
||||
# Nexus NX — 统一运维菜单(安装 + 升级 + 日常运维)
|
||||
#
|
||||
# bash deploy/nx 交互菜单
|
||||
# bash deploy/nx install 同菜单「安装」
|
||||
# bash deploy/nx god 上帝菜单(运维)
|
||||
# nx 安装后链到 /usr/local/bin/nx
|
||||
# nx 交互菜单
|
||||
# nx update 一键更新(等同 deploy/update.sh)
|
||||
# nx update --no-cache 无缓存重建镜像
|
||||
# nx install-fresh 全新安装
|
||||
# nx health 健康检查
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# 经 /usr/local/bin/nx 符号链接调用时,须 readlink 才能找到 deploy/ 目录
|
||||
NX_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
NX_DIR="$(cd "$(dirname "$NX_SELF")" && pwd)"
|
||||
# shellcheck source=./nexus-1panel.sh
|
||||
@@ -34,7 +34,11 @@ detect_runtime() {
|
||||
local prof q
|
||||
prof="$(profile_env_file "$NEXUS_ROOT")"
|
||||
if [[ -f "$prof" && -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
||||
q="$(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" \
|
||||
local -a det_args=(-f "$COMPOSE_FILE")
|
||||
if has_1panel_network && [[ -f "$NEXUS_ROOT/$COMPOSE_1PANEL" ]]; then
|
||||
det_args+=(-f "$COMPOSE_1PANEL")
|
||||
fi
|
||||
q="$(cd "$NEXUS_ROOT" && docker compose "${det_args[@]}" \
|
||||
--env-file "$ENV_PROD" --env-file "$prof" ps -q nexus 2>/dev/null || true)"
|
||||
if [[ -n "$q" ]]; then
|
||||
NX_RUNTIME="docker"
|
||||
@@ -50,11 +54,8 @@ detect_runtime() {
|
||||
}
|
||||
|
||||
install_nx_cli() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
ln -sf "${NX_DIR}/nx" /usr/local/bin/nx 2>/dev/null || true
|
||||
chmod +x "${NX_DIR}/nx" 2>/dev/null || true
|
||||
NEXUS_ROOT="$(cd "${NX_DIR}/.." && pwd)" \
|
||||
bash "${NX_DIR}/install-nx-cli.sh" 2>/dev/null || true
|
||||
}
|
||||
|
||||
menu_header() {
|
||||
@@ -62,7 +63,7 @@ menu_header() {
|
||||
clear_screen
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Nexus NX — ${title}"
|
||||
printf "║ Nexus NX — %-44s║\n" "$title"
|
||||
echo "╠══════════════════════════════════════════════════════════╣"
|
||||
echo "║ 域名: ${NEXUS_DOMAIN} 目录: ${NEXUS_ROOT}"
|
||||
if is_stack_installed; then
|
||||
@@ -99,7 +100,7 @@ run_install() {
|
||||
|
||||
menu_install() {
|
||||
while true; do
|
||||
menu_header "安装向导"
|
||||
menu_header "安装"
|
||||
cat <<'EOF'
|
||||
[1] 一键安装(默认 2核8G / 2c8g)
|
||||
[2] 安装 — 4核16G(4c16g)
|
||||
@@ -108,9 +109,7 @@ menu_install() {
|
||||
[5] 已 clone 到 /opt/nexus,仅 pull + 启动(--skip-clone)
|
||||
[6] 全新安装(install-nexus-fresh.sh → /app/install.html)
|
||||
[7] 检查端口 / 防火墙(不安装)
|
||||
[8] 写入 Gitea 令牌(init-token)
|
||||
[9] 返回主菜单
|
||||
[0] 退出
|
||||
[0] 返回
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
@@ -142,19 +141,13 @@ EOF
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
8)
|
||||
read -r -p "Gitea Access Token: " tok
|
||||
cmd_init_token "$tok"
|
||||
pause_enter
|
||||
;;
|
||||
9) return 0 ;;
|
||||
0) exit 0 ;;
|
||||
0) return 0 ;;
|
||||
*) warn "无效选项" ; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
god_restart_nexus() {
|
||||
ops_restart_nexus() {
|
||||
step "重启 Nexus(Python)..."
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" restart nexus
|
||||
@@ -164,32 +157,32 @@ god_restart_nexus() {
|
||||
error "未检测到运行中的 Nexus"
|
||||
return 1
|
||||
fi
|
||||
info "已发送重启"
|
||||
sleep 3
|
||||
curl -sf "http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")/health" && info "/health → ok" || warn "健康检查未通过,请查看日志"
|
||||
verify_health "$NEXUS_ROOT" "重启" || true
|
||||
}
|
||||
|
||||
god_restart_stack() {
|
||||
step "重启全部 Compose 服务..."
|
||||
ops_restart_stack() {
|
||||
step "重启 Compose 栈..."
|
||||
compose_cmd "$NEXUS_ROOT" restart
|
||||
sleep 5
|
||||
verify_health "$NEXUS_ROOT" "重启" || true
|
||||
}
|
||||
|
||||
god_stop_stack() {
|
||||
warn "将停止 MySQL / Redis / Nexus 全部容器"
|
||||
ops_stop_stack() {
|
||||
warn "将停止 Nexus 容器(MySQL/Redis 为外置服务,不受影响)"
|
||||
read -r -p "确认? [y/N]: " c
|
||||
[[ "$c" == "y" || "$c" == "Y" ]] || return 0
|
||||
compose_cmd "$NEXUS_ROOT" stop
|
||||
info "已停止"
|
||||
}
|
||||
|
||||
god_start_stack() {
|
||||
compose_cmd "$NEXUS_ROOT" up -d
|
||||
ops_start_stack() {
|
||||
compose_cmd "$NEXUS_ROOT" up -d --remove-orphans
|
||||
verify_nexus_1panel_network "$NEXUS_ROOT" || return 1
|
||||
verify_health "$NEXUS_ROOT" "启动" || true
|
||||
}
|
||||
|
||||
god_logs() {
|
||||
ops_logs() {
|
||||
local svc="${1:-nexus}"
|
||||
echo ""
|
||||
read -r -p "跟踪日志行数 [80]: " n
|
||||
@@ -197,146 +190,191 @@ god_logs() {
|
||||
compose_cmd "$NEXUS_ROOT" logs --tail="$n" -f "$svc"
|
||||
}
|
||||
|
||||
god_status() {
|
||||
ops_status() {
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" ps
|
||||
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
|
||||
supervisorctl status nexus
|
||||
fi
|
||||
echo ""
|
||||
local port h
|
||||
local port h code
|
||||
port="$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
h=$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || echo "fail")
|
||||
info "/health → ${h}"
|
||||
curl -s -o /dev/null -w " /app/ HTTP %{http_code}\n" "http://127.0.0.1:${port}/app/" 2>/dev/null || true
|
||||
local cname
|
||||
cname="$(nexus_container_name 2>/dev/null || true)"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||||
if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
|
||||
if [[ "$code" == "404" ]]; then
|
||||
info "/app/install.html → 已归档 (HTTP 404)"
|
||||
else
|
||||
warn "/app/install.html → 应已归档但 HTTP ${code}"
|
||||
fi
|
||||
else
|
||||
info "/app/install.html → HTTP ${code}"
|
||||
fi
|
||||
}
|
||||
|
||||
god_backup_now() {
|
||||
ops_backup_now() {
|
||||
NO_BACKUP=false
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
}
|
||||
|
||||
god_upgrade_interactive() {
|
||||
ops_install_cron() {
|
||||
bash "${NX_DIR}/install_ops_cron.sh"
|
||||
}
|
||||
|
||||
ops_update() {
|
||||
local args=()
|
||||
PRUNE_IMAGES=false
|
||||
NO_BACKUP=false
|
||||
NO_CACHE=false
|
||||
read -r -p "跳过 MySQL 备份? [y/N]: " nb
|
||||
[[ "$nb" == "y" || "$nb" == "Y" ]] && NO_BACKUP=true
|
||||
[[ "$nb" == "y" || "$nb" == "Y" ]] && args+=(--no-backup)
|
||||
read -r -p "无缓存重建镜像? [y/N]: " nc
|
||||
[[ "$nc" == "y" || "$nc" == "Y" ]] && args+=(--no-cache)
|
||||
read -r -p "升级后清理悬空镜像? [y/N]: " pr
|
||||
[[ "$pr" == "y" || "$pr" == "Y" ]] && PRUNE_IMAGES=true
|
||||
cmd_upgrade
|
||||
[[ "$pr" == "y" || "$pr" == "Y" ]] && args+=(--prune)
|
||||
bash "${NX_DIR}/update.sh" "${args[@]}"
|
||||
pause_enter
|
||||
}
|
||||
|
||||
menu_god() {
|
||||
require_installed() {
|
||||
if ! is_stack_installed; then
|
||||
warn "尚未安装,请先使用安装向导"
|
||||
warn "尚未安装,请先使用菜单 [1] 安装"
|
||||
pause_enter
|
||||
return 0
|
||||
return 1
|
||||
fi
|
||||
require_root
|
||||
load_secrets
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
detect_runtime
|
||||
|
||||
while true; do
|
||||
menu_header "上帝菜单 · 运维"
|
||||
cat <<EOF
|
||||
── 服务 ──
|
||||
[1] 重启 Nexus(Python 应用)
|
||||
[2] 重启全部容器(mysql + redis + nexus)
|
||||
[3] 启动 Compose 栈
|
||||
[4] 停止 Compose 栈
|
||||
[5] 重建 Nexus 镜像并启动(up -d --build nexus)
|
||||
|
||||
── 观测 ──
|
||||
[6] 状态 + 健康检查
|
||||
[7] 查看 Nexus 日志(跟踪)
|
||||
[8] 查看 MySQL / Redis 日志
|
||||
[9] 端口 / 防火墙检查
|
||||
|
||||
── 发布 ──
|
||||
[a] 检查 Git 更新
|
||||
[b] 一键升级(备份 + pull + 重建)
|
||||
[c] 立即备份 MySQL
|
||||
|
||||
── 其他 ──
|
||||
[d] 进入 Nexus 容器 shell
|
||||
[e] 打开 1Panel 反代说明
|
||||
[0] 返回主菜单
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) god_restart_nexus; pause_enter ;;
|
||||
2) god_restart_stack; pause_enter ;;
|
||||
3) god_start_stack; pause_enter ;;
|
||||
4) god_stop_stack; pause_enter ;;
|
||||
5)
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build nexus
|
||||
verify_health "$NEXUS_ROOT" "重建" || true
|
||||
pause_enter
|
||||
;;
|
||||
6) god_status; pause_enter ;;
|
||||
7) god_logs nexus ;;
|
||||
8)
|
||||
echo "1) mysql 2) redis"
|
||||
read -r -p "选: " s
|
||||
[[ "$s" == "1" ]] && god_logs mysql || god_logs redis
|
||||
;;
|
||||
9)
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
check_ports_post "$NEXUS_ROOT"
|
||||
pause_enter
|
||||
;;
|
||||
a|A)
|
||||
cmd_check
|
||||
pause_enter
|
||||
;;
|
||||
b|B) god_upgrade_interactive ;;
|
||||
c|C) god_backup_now; pause_enter ;;
|
||||
d)
|
||||
compose_cmd "$NEXUS_ROOT" exec nexus bash || compose_cmd "$NEXUS_ROOT" exec nexus sh
|
||||
;;
|
||||
e)
|
||||
info "1Panel → 网站 → 创建站点 ${NEXUS_DOMAIN}"
|
||||
info "反向代理: http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
info "示例配置: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example"
|
||||
pause_enter
|
||||
;;
|
||||
0) return 0 ;;
|
||||
*) warn "无效选项"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
menu_main() {
|
||||
load_secrets
|
||||
while true; do
|
||||
menu_header "主菜单"
|
||||
if is_stack_installed; then
|
||||
echo " 提示: 安装完成后请用 [2] 上帝菜单 重启 Python / 升级 / 看日志"
|
||||
echo ""
|
||||
fi
|
||||
menu_header "运维菜单"
|
||||
cat <<'EOF'
|
||||
[1] 安装向导(新机 / 迁机)
|
||||
[2] 上帝菜单(重启 Python · 升级 · 日志 · 备份)
|
||||
[3] 快速健康检查
|
||||
── 安装 ──
|
||||
[1] 安装向导(新机 / 迁机 / 档位)
|
||||
[2] 全新安装 → /app/install.html
|
||||
|
||||
── 日常运维(需已安装)──
|
||||
[3] 状态 + 健康检查
|
||||
[4] 一键更新(pull + 重建镜像)★
|
||||
[5] 重启 Nexus(Python)
|
||||
[6] 查看 Nexus 日志
|
||||
[7] 启动 / 停止 Compose 栈
|
||||
[8] 重建镜像并启动(--build nexus)
|
||||
[9] 备份 MySQL
|
||||
[e] 安装巡检/备份 cron(health_monitor + db_backup)
|
||||
[v] 1Panel 安装/运维验收脚本
|
||||
|
||||
── 其它 ──
|
||||
[a] 检查 Git 是否有更新
|
||||
[b] 端口 / 防火墙检查
|
||||
[c] 进入 Nexus 容器 shell
|
||||
[d] 1Panel 反代说明
|
||||
[0] 退出
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) menu_install ;;
|
||||
2) menu_god ;;
|
||||
2)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
warn "将执行全新安装(清除 nexus-state 卷内旧配置并重建容器)"
|
||||
read -r -p "确认继续? [y/N]: " fresh_ok
|
||||
[[ "$fresh_ok" == "y" || "$fresh_ok" == "Y" ]] || continue
|
||||
fi
|
||||
exec bash "${NX_DIR}/install-nexus-fresh.sh" --profile "${NEXUS_PROFILE:-2c8g}"
|
||||
;;
|
||||
3)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
god_status
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
detect_runtime
|
||||
ops_status
|
||||
else
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
4)
|
||||
require_installed || continue
|
||||
ops_update
|
||||
;;
|
||||
5)
|
||||
require_installed || continue
|
||||
ops_restart_nexus
|
||||
pause_enter
|
||||
;;
|
||||
6)
|
||||
require_installed || continue
|
||||
ops_logs nexus
|
||||
;;
|
||||
7)
|
||||
require_installed || continue
|
||||
echo " [1] 启动 [2] 停止 [3] 重启栈"
|
||||
read -r -p "选: " s
|
||||
case "$s" in
|
||||
1) ops_start_stack ;;
|
||||
2) ops_stop_stack ;;
|
||||
3) ops_restart_stack ;;
|
||||
*) warn "无效" ;;
|
||||
esac
|
||||
pause_enter
|
||||
;;
|
||||
8)
|
||||
require_installed || continue
|
||||
read -r -p "无缓存重建? [y/N]: " nc
|
||||
if [[ "$nc" == "y" || "$nc" == "Y" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" build --no-cache nexus
|
||||
fi
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build --remove-orphans nexus
|
||||
verify_nexus_1panel_network "$NEXUS_ROOT" || true
|
||||
verify_health "$NEXUS_ROOT" "重建" || true
|
||||
pause_enter
|
||||
;;
|
||||
9)
|
||||
require_installed || continue
|
||||
ops_backup_now
|
||||
pause_enter
|
||||
;;
|
||||
e|E)
|
||||
require_root
|
||||
ops_install_cron
|
||||
pause_enter
|
||||
;;
|
||||
v|V)
|
||||
require_root
|
||||
bash "${NX_DIR}/verify-1panel-install-wizard.sh"
|
||||
pause_enter
|
||||
;;
|
||||
a|A)
|
||||
require_root
|
||||
cmd_check
|
||||
pause_enter
|
||||
;;
|
||||
b|B)
|
||||
require_root
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
is_stack_installed && check_ports_post "$NEXUS_ROOT" || true
|
||||
pause_enter
|
||||
;;
|
||||
c|C)
|
||||
require_installed || continue
|
||||
compose_cmd "$NEXUS_ROOT" exec nexus bash || compose_cmd "$NEXUS_ROOT" exec nexus sh
|
||||
;;
|
||||
d|D)
|
||||
info "1Panel → 网站 → 创建站点 ${NEXUS_DOMAIN}"
|
||||
info "反向代理: http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
info "示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example"
|
||||
pause_enter
|
||||
;;
|
||||
0) exit 0 ;;
|
||||
*) warn "无效选项"; sleep 1 ;;
|
||||
esac
|
||||
@@ -345,11 +383,21 @@ EOF
|
||||
|
||||
nx_main() {
|
||||
local sub="${1:-}"
|
||||
install_nx_cli
|
||||
load_secrets
|
||||
case "$sub" in
|
||||
""|menu) menu_main ;;
|
||||
""|menu|god|ops|admin)
|
||||
# god/ops 已合并进主菜单,保留别名兼容
|
||||
[[ "$sub" == "god" || "$sub" == "ops" || "$sub" == "admin" ]] && \
|
||||
warn "「上帝菜单」已合并进 nx 主菜单,直接选对应项即可"
|
||||
menu_main
|
||||
;;
|
||||
install) menu_install ;;
|
||||
god|ops|admin) menu_god ;;
|
||||
update|upgrade)
|
||||
require_root
|
||||
shift || true
|
||||
exec bash "${NX_DIR}/update.sh" "$@"
|
||||
;;
|
||||
install-fresh|fresh)
|
||||
require_root
|
||||
exec bash "${NX_DIR}/install-nexus-fresh.sh" "$@"
|
||||
@@ -357,31 +405,44 @@ nx_main() {
|
||||
install-auto|auto)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
run_install "${NEXUS_PROFILE:-2c8g}" skip-clone
|
||||
warn "已安装,install-auto 将执行升级而非重复安装"
|
||||
exec bash "${NX_DIR}/update.sh"
|
||||
else
|
||||
run_install "${NEXUS_PROFILE:-2c8g}" ""
|
||||
fi
|
||||
;;
|
||||
health)
|
||||
require_root
|
||||
is_stack_installed && god_status || check_ports_preflight "$NEXUS_ROOT"
|
||||
is_stack_installed && ops_status || check_ports_preflight "$NEXUS_ROOT"
|
||||
;;
|
||||
cron)
|
||||
require_root
|
||||
ops_install_cron
|
||||
;;
|
||||
verify)
|
||||
require_root
|
||||
bash "${NX_DIR}/verify-1panel-install-wizard.sh"
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
用法: nx [子命令]
|
||||
|
||||
(无) 交互主菜单
|
||||
install 安装向导
|
||||
install-fresh 全新安装(1Panel 风格,/app/install.html)
|
||||
install-auto 非交互安装已有密钥(2c8g)
|
||||
god 上帝菜单(重启/升级/日志)
|
||||
health 健康检查
|
||||
(无) 统一运维菜单(安装 + 升级 + 重启 + 日志)
|
||||
update 一键更新(等同 bash deploy/update.sh)
|
||||
install-fresh 全新安装(/app/install.html)
|
||||
install-auto 非交互安装(默认 2c8g)
|
||||
health 健康检查
|
||||
cron 安装 health_monitor + db_backup crontab
|
||||
verify 运行 1Panel 验收脚本
|
||||
|
||||
底层仍可使用: bash deploy/nexus-1panel.sh install|upgrade|check|ports
|
||||
god / ops 已合并进主菜单(兼容别名)
|
||||
|
||||
curl 入口:
|
||||
安装 curl -fsSL .../deploy/quick-install.sh | bash
|
||||
更新 curl -fsSL .../deploy/update.sh | bash
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
# 兼容 nexus-1panel.sh 子命令
|
||||
main "$@"
|
||||
;;
|
||||
esac
|
||||
|
||||
+69
-20
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Nexus Pre-deploy Gate Check (v2 — 7 gates)
|
||||
# 在部署前强制检查 7 道门控,任何一道不过则阻止部署
|
||||
# Nexus Pre-deploy Gate Check (v3 — 8 gates)
|
||||
# 在部署前强制检查 8 道门控,任何一道不过则阻止部署
|
||||
#
|
||||
# 门控1: CHANGELOG — docs/changelog/ 下必须有今天的 .md 文件(且行数≥10)
|
||||
# 门控2: AUDIT — docs/audit/ 下必须有今天的审计记录(且包含关键段落)
|
||||
@@ -9,6 +9,7 @@
|
||||
# 门控5: IMPORT — python -c "import server.main" 成功
|
||||
# 门控6: SECURITY — bandit -r server/ 无 HIGH/MEDIUM
|
||||
# 门控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
|
||||
# 退出码: 0=全部通过, 1=至少一道门未过
|
||||
@@ -22,7 +23,7 @@ DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
TIMESTAMP=$(date -Iseconds)
|
||||
GATES_PASSED=0
|
||||
GATES_TOTAL=7
|
||||
GATES_TOTAL=8
|
||||
BLOCKED=0
|
||||
GATE_RESULTS=""
|
||||
|
||||
@@ -53,14 +54,14 @@ gate_log() {
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo " Nexus Pre-deploy Gate Check (v2)"
|
||||
echo " Nexus Pre-deploy Gate Check (v3)"
|
||||
echo " Date: ${TODAY}"
|
||||
echo " Gates: ${GATES_TOTAL}"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# ── Gate 1: Changelog (存在 + 行数≥10) ──
|
||||
echo -n "Gate 1/7: Changelog ... "
|
||||
echo -n "Gate 1/8: Changelog ... "
|
||||
CHANGELOG_DIR="${DEPLOY_DIR}/docs/changelog"
|
||||
if ls "${CHANGELOG_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${CHANGELOG_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
@@ -86,7 +87,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 2: Audit (存在 + 关键段落) ──
|
||||
echo -n "Gate 2/7: Audit ... "
|
||||
echo -n "Gate 2/8: Audit ... "
|
||||
AUDIT_DIR="${DEPLOY_DIR}/docs/audit"
|
||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
@@ -117,7 +118,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 3: Test (必须存在 + 必须通过) ──
|
||||
echo -n "Gate 3/7: Test ... "
|
||||
echo -n "Gate 3/8: Test ... "
|
||||
TEST_SCRIPT="${DEPLOY_DIR}/tests/test_api.py"
|
||||
if [ ! -f "${TEST_SCRIPT}" ]; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
@@ -126,15 +127,43 @@ if [ ! -f "${TEST_SCRIPT}" ]; then
|
||||
BLOCKED=1
|
||||
gate_log "test" "BLOCK" "script not found"
|
||||
else
|
||||
TEST_OUTPUT=$(cd "${DEPLOY_DIR}" && python3 "${TEST_SCRIPT}" 2>&1) || true
|
||||
# Always hit local API for gate 3 (ignore shell NEXUS_TEST_BASE used by import/e2e scripts)
|
||||
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=${FAIL_COUNT:-0}
|
||||
GATE3_PASSED=0
|
||||
if [ "${FAIL_COUNT}" -gt 0 ] || echo "${TEST_OUTPUT}" | grep -qE "[0-9]+ test(s) failed"; 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"
|
||||
# 本机未起 :8600 时,若失败均为 Connection refused,回退 pytest 专项
|
||||
NON_CONN_FAILS=$(echo "${TEST_OUTPUT}" | grep -E "^\s+\[FAIL\]" | grep -vcE "Connection refused|Errno 111" 2>/dev/null || true)
|
||||
NON_CONN_FAILS=${NON_CONN_FAILS:-0}
|
||||
if [ "${FAIL_COUNT}" -gt 0 ] && [ "${NON_CONN_FAILS}" -eq 0 ]; then
|
||||
PYTEST_BIN=$(_pick_bin pytest || true)
|
||||
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
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ All tests passed"
|
||||
@@ -144,7 +173,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 4: Lint (ruff — F only: undefined names, unused imports) ──
|
||||
echo -n "Gate 4/7: Lint ... "
|
||||
echo -n "Gate 4/8: Lint ... "
|
||||
RUFF_BIN=$(_pick_bin ruff || true)
|
||||
if [ -n "${RUFF_BIN}" ]; then
|
||||
LINT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${RUFF_BIN}" check server/ --select F 2>&1) || true
|
||||
@@ -171,7 +200,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 5: Import (python -c "import server.main") ──
|
||||
echo -n "Gate 5/7: Import ... "
|
||||
echo -n "Gate 5/8: Import ... "
|
||||
IMPORT_PYTHON=""
|
||||
for p in "${DEPLOY_DIR}/.venv/bin/python3" "${DEPLOY_DIR}/venv/bin/python3"; do
|
||||
if [ -x "${p}" ]; then
|
||||
@@ -206,7 +235,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 6: Security (bandit — HIGH only) ──
|
||||
echo -n "Gate 6/7: Security ... "
|
||||
echo -n "Gate 6/8: Security ... "
|
||||
BANDIT_BIN=$(_pick_bin bandit || true)
|
||||
if [ -n "${BANDIT_BIN}" ]; then
|
||||
SEC_OUTPUT=$(cd "${DEPLOY_DIR}" && "${BANDIT_BIN}" -r server/ -f txt -ll 2>&1) || true
|
||||
@@ -233,18 +262,18 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 7: Review (审计文件交叉验证) ──
|
||||
echo -n "Gate 7/7: Review ... "
|
||||
echo -n "Gate 7/8: Review ... "
|
||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
# 检查审计文件中是否列出了实际改动的文件
|
||||
REVIEW_OK=1
|
||||
# 如果是git仓库,交叉验证改动文件
|
||||
if command -v git &>/dev/null && [ -d "${DEPLOY_DIR}/.git" ]; then
|
||||
CHANGED_FILES=$(cd "${DEPLOY_DIR}" && git diff --name-only HEAD~1 2>/dev/null || true)
|
||||
CHANGED_FILES=$(cd "${DEPLOY_DIR}" && git diff --name-only HEAD~1..HEAD 2>/dev/null || true)
|
||||
for CF in ${CHANGED_FILES}; do
|
||||
# 跳过非代码文件
|
||||
# 跳过非代码文件与门控运行日志
|
||||
case "${CF}" in
|
||||
docs/*|CLAUDE.md|*.md) continue ;;
|
||||
docs/*|CLAUDE.md|*.md|deploy/gate_log.jsonl|web/app/assets/*) continue ;;
|
||||
esac
|
||||
# 检查审计文件是否提到了这个文件
|
||||
if ! grep -q "$(basename "${CF}")" "${FILE}" 2>/dev/null; then
|
||||
@@ -272,6 +301,26 @@ else
|
||||
gate_log "review" "BLOCK" "no audit file"
|
||||
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 ──
|
||||
echo -n "Pre-flight: Shell EOL (LF) ... "
|
||||
if bash "${DEPLOY_DIR}/deploy/check_shell_eol.sh" >/dev/null 2>&1; then
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/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
+8
-7
@@ -3,10 +3,10 @@
|
||||
# Nexus 6.0 — 公共仓库一键安装入口(参考 1Panel quick_start.sh)
|
||||
#
|
||||
# root 登录后一条命令:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
#
|
||||
# 或保存后执行:
|
||||
# curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" -o /tmp/quick-install.sh
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" -o /tmp/quick-install.sh
|
||||
# bash /tmp/quick-install.sh
|
||||
#
|
||||
# 可选参数会传给 install-nexus-fresh.sh,例如:
|
||||
@@ -15,11 +15,12 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
|
||||
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
@@ -57,7 +58,7 @@ download_script() {
|
||||
local url base
|
||||
for base in \
|
||||
"${NEXUS_RAW_BASE}" \
|
||||
"http://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
"${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/${name}"
|
||||
if curl -fsSL "$url" -o "$dest" 2>/dev/null && validate_script_file "$dest"; then
|
||||
info "已下载 ${name}"
|
||||
@@ -68,9 +69,9 @@ download_script() {
|
||||
}
|
||||
|
||||
run_via_git() {
|
||||
local url="http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
local url="${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
url="http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
url="${NEXUS_GITEA_SCHEME}://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
step "改用 git clone..."
|
||||
mkdir -p "$(dirname "$NEXUS_ROOT")"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/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='node_modules'
|
||||
--exclude='.venv'
|
||||
--exclude='venv'
|
||||
--exclude='__pycache__'
|
||||
--exclude='.cursor'
|
||||
--exclude='.install_locked'
|
||||
--exclude='.install_state.json'
|
||||
--exclude='web/app/assets'
|
||||
--exclude='tmp'
|
||||
--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
+25
-5
@@ -5,6 +5,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
CONTAINER="${NEXUS_CONTAINER:-}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
@@ -33,13 +34,33 @@ sync_file() {
|
||||
docker cp "$src" "${CONTAINER}:${dest}"
|
||||
}
|
||||
|
||||
wizard_archived_in_container() {
|
||||
docker exec "$CONTAINER" test -f /app/.install_locked 2>/dev/null \
|
||||
|| docker exec "$CONTAINER" test -f /var/lib/nexus/.install_locked 2>/dev/null \
|
||||
|| docker exec "$CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null
|
||||
}
|
||||
|
||||
main() {
|
||||
local app="$NEXUS_ROOT/web/app"
|
||||
local wizard_archived=false
|
||||
if [[ ! -f "$app/install.html" ]]; then
|
||||
error "仓库中无 $app/install.html,请先 git pull"
|
||||
exit 1
|
||||
fi
|
||||
resolve_container
|
||||
if wizard_archived_in_container; then
|
||||
wizard_archived=true
|
||||
info "安装已锁定或向导已归档,跳过静态/Python 热同步"
|
||||
local code
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "404" ]]; then
|
||||
info "/app/install.html 已归档 → HTTP 404"
|
||||
exit 0
|
||||
fi
|
||||
warn "/app/install.html 应已归档但返回 HTTP $code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "同步安装向导静态文件..."
|
||||
sync_file "$app/install.html" /app/web/app/install.html
|
||||
sync_file "$app/vendor/tailwindcss-browser.js" /app/web/app/vendor/tailwindcss-browser.js
|
||||
@@ -47,14 +68,14 @@ main() {
|
||||
sync_file "$app/vendor/theme.css" /app/web/app/vendor/theme.css
|
||||
sync_file "$app/vendor/alpinejs.min.js" /app/web/app/vendor/alpinejs.min.js
|
||||
|
||||
local install_py="$NEXUS_ROOT/server/api/install.py"
|
||||
local install_py="$NEXUS_ROOT/server/api/install.py" code
|
||||
if [[ -f "$install_py" ]]; then
|
||||
info "同步 install.py(Docker Redis env-check 修复)..."
|
||||
sync_file "$install_py" /app/server/api/install.py
|
||||
info "重启 Nexus 容器以加载 Python 变更..."
|
||||
docker restart "$CONTAINER" >/dev/null
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -sf http://127.0.0.1:8600/health >/dev/null 2>&1; then
|
||||
if curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/health" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
@@ -63,12 +84,11 @@ main() {
|
||||
warn "未找到 $install_py,跳过 Python 热同步"
|
||||
fi
|
||||
|
||||
local code
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/install.html 2>/dev/null || echo 000)"
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "200" ]]; then
|
||||
info "/app/install.html → HTTP 200"
|
||||
else
|
||||
warn "/app/install.html → HTTP $code(请检查 Nexus 是否在 8600 监听)"
|
||||
warn "/app/install.html → HTTP $code(请检查 Nexus 是否在 ${NEXUS_PUBLISH_PORT} 监听)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/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 prefix html bundle_path code
|
||||
prefix="$1"
|
||||
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${prefix}/" 2>/dev/null || true)"
|
||||
bundle_path="$(printf '%s' "$html" | grep -oE "${prefix}"'/assets/index-[^"]+\.js' | head -1 || true)"
|
||||
if [[ -z "$bundle_path" ]]; then
|
||||
error "${prefix}/ 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"
|
||||
}
|
||||
|
||||
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 "/app"
|
||||
verify_entry_bundle "/app-v2"
|
||||
info "web/app 与 web/app-v2 同步完成"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Diagnose 1Panel Redis auth from Nexus container network perspective.
|
||||
# Usage:
|
||||
# REDIS_PASS='redis_WP3NyN' bash deploy/test-1panel-redis.sh
|
||||
# bash deploy/test-1panel-redis.sh # prompts for password
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
ENV_PROD="${NEXUS_ROOT}/docker/.env.prod"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
ok() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
fail() { echo -e "\033[0;31m[FAIL]\033[0m $*"; }
|
||||
|
||||
pick_redis_container() {
|
||||
if [[ -n "${REDIS_CONTAINER:-}" ]]; then
|
||||
echo "$REDIS_CONTAINER"
|
||||
return 0
|
||||
fi
|
||||
local h
|
||||
h="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$h" ]]; then
|
||||
echo "$h"
|
||||
return 0
|
||||
fi
|
||||
if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then
|
||||
h="$("$NEXUS_ROOT/deploy/detect-1panel-services.sh" 2>/dev/null | sed -n 's/^NEXUS_1PANEL_REDIS_HOST=//p' | head -1)"
|
||||
[[ -n "$h" ]] && echo "$h" && return 0
|
||||
fi
|
||||
docker ps --format '{{.Names}}' | grep -iE '^1Panel-redis-' | head -1 || true
|
||||
}
|
||||
|
||||
try_ping() {
|
||||
local label="$1" url="$2"
|
||||
local nexus
|
||||
nexus="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$nexus" ]]; then
|
||||
fail "未找到 Nexus 容器"
|
||||
return 1
|
||||
fi
|
||||
if docker exec "$nexus" python3 -c "
|
||||
import redis
|
||||
r = redis.Redis.from_url('${url}', socket_timeout=3)
|
||||
print(r.ping())
|
||||
" 2>/dev/null | grep -q True; then
|
||||
ok "$label → $url"
|
||||
return 0
|
||||
fi
|
||||
fail "$label"
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
local redis_host pass
|
||||
redis_host="$(pick_redis_container)"
|
||||
[[ -n "$redis_host" ]] || { fail "未找到 Redis 容器/主机名"; exit 1; }
|
||||
info "Redis 主机: $redis_host"
|
||||
|
||||
if [[ -z "${REDIS_PASS:-}" ]]; then
|
||||
read -r -s -p "1Panel Redis 密码(应用参数): " pass
|
||||
echo ""
|
||||
else
|
||||
pass="$REDIS_PASS"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "从 Nexus 容器内尝试多种 URL 格式(807f4c2 修复后)..."
|
||||
local any=0
|
||||
try_ping "无认证" "redis://${redis_host}:6379/0" && any=1 || true
|
||||
if [[ -n "$pass" ]]; then
|
||||
try_ping "仅密码 (:pass@)" "redis://:${pass}@${redis_host}:6379/0" && any=1 || true
|
||||
try_ping "default 用户" "redis://default:${pass}@${redis_host}:6379/0" && any=1 || true
|
||||
info "跳过 root 用户探测 — 1Panel Redis 无 root 账号"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$any" -eq 1 ]]; then
|
||||
ok "至少一种格式可用"
|
||||
echo ""
|
||||
info "安装向导步骤 3:1Panel Redis 无账号,只填密码(后端自动用 :pass@ 或 default)"
|
||||
info "已有 .env 时写入 NEXUS_REDIS_URL,例如:"
|
||||
info " redis://:${pass}@${redis_host}:6379/0"
|
||||
info " 或 redis://default:${pass}@${redis_host}:6379/0"
|
||||
exit 0
|
||||
fi
|
||||
fail "全部失败。请在 1Panel → Redis → 参数 核对密码,或检查 Nexus 是否在 1panel-network"
|
||||
info "本机: docker exec -it ${redis_host} redis-cli -u 'redis://:密码@127.0.0.1:6379' ping"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 卸载 Nexus 旧版 Compose 内置 MySQL 容器
|
||||
#
|
||||
# bash deploy/uninstall-mysql-compose.sh
|
||||
# bash deploy/uninstall-mysql-compose.sh --purge-volume
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
|
||||
PURGE_VOLUME=false
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
卸载 Nexus Compose 遗留 MySQL 容器
|
||||
|
||||
bash deploy/uninstall-mysql-compose.sh [--purge-volume]
|
||||
|
||||
说明:
|
||||
- 不卸载宿主机/1Panel 自建的 MySQL
|
||||
- 卸载后请自建库 nexus + 用户,安装向导步骤 3 填写连接信息
|
||||
EOF
|
||||
}
|
||||
|
||||
compose_env_args() {
|
||||
local_env=()
|
||||
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
|
||||
local prof="${NEXUS_ROOT}/docker/.host-profile"
|
||||
if [[ -f "$prof" ]]; then
|
||||
local p
|
||||
p="$(tr -d '\r\n' <"$prof")"
|
||||
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
|
||||
fi
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--purge-volume) PURGE_VOLUME=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ "$(id -u)" -eq 0 ]] || { error "请使用 root 执行"; exit 1; }
|
||||
|
||||
removed=0
|
||||
|
||||
stop_rm_container() {
|
||||
local name="$1"
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
|
||||
info "停止并删除容器: $name"
|
||||
docker stop "$name" >/dev/null 2>&1 || true
|
||||
docker rm -f "$name" >/dev/null 2>&1 || true
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
for pattern in nexus-prod-mysql-1 nexus-prod-mysql nexus-mysql-1; do
|
||||
stop_rm_container "$pattern"
|
||||
done
|
||||
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
|
||||
compose_env_args
|
||||
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]] && grep -q '^ mysql:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
|
||||
warn "compose 仍含 mysql 服务,尝试 compose rm..."
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop mysql 2>/dev/null || true)
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f mysql 2>/dev/null || true)
|
||||
removed=$((removed + 1))
|
||||
elif [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" && -f "${NEXUS_ROOT}/docker/.env.prod" ]]; then
|
||||
info "Compose --remove-orphans(从 nexus-prod 项目移除 mysql 孤儿容器)..."
|
||||
compose_files=(-f "${NEXUS_ROOT}/${COMPOSE_FILE}")
|
||||
if docker network inspect 1panel-network >/dev/null 2>&1 \
|
||||
&& [[ -f "${NEXUS_ROOT}/docker/docker-compose.1panel.yml" ]]; then
|
||||
compose_files+=(-f "${NEXUS_ROOT}/docker/docker-compose.1panel.yml")
|
||||
fi
|
||||
(cd "$NEXUS_ROOT" && docker compose "${compose_files[@]}" "${local_env[@]}" up -d --remove-orphans 2>/dev/null || true)
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
fi
|
||||
|
||||
if [[ "$PURGE_VOLUME" == true ]]; then
|
||||
while IFS= read -r vol; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
info "删除数据卷: $vol"
|
||||
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol"
|
||||
done < <(docker volume ls -q | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
fi
|
||||
|
||||
leftover="$(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)"
|
||||
if [[ -n "$leftover" ]]; then
|
||||
warn "仍有 MySQL 相关容器:"
|
||||
echo "$leftover" | sed 's/^/ /'
|
||||
warn "若确认为 Compose 遗留,可: docker rm -f <容器名>"
|
||||
elif [[ "$removed" -gt 0 ]]; then
|
||||
info "Compose MySQL 容器已卸载。"
|
||||
else
|
||||
info "未发现 Nexus Compose MySQL 容器。"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "当前 Nexus 栈:"
|
||||
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | grep -i nexus || docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
|
||||
echo ""
|
||||
info "下一步:1Panel 应用商店安装 MySQL → nx update 探测容器名 → 向导步骤 3 使用 1Panel-mysql-xxxx"
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 卸载 Nexus 旧版 Compose 内置 Redis 容器
|
||||
#
|
||||
# 适用:已从 docker-compose.prod.yml 移除 redis 服务,改用宿主机/1Panel 自建 Redis。
|
||||
# 不会卸载 apt/1Panel 安装的 Redis,仅删除 Docker 容器与(可选)数据卷。
|
||||
#
|
||||
# bash deploy/uninstall-redis-compose.sh
|
||||
# bash deploy/uninstall-redis-compose.sh --purge-volume # 同时删 redis-data 卷
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
|
||||
PURGE_VOLUME=false
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
卸载 Nexus Compose 遗留 Redis 容器
|
||||
|
||||
bash deploy/uninstall-redis-compose.sh [--purge-volume]
|
||||
|
||||
选项:
|
||||
--purge-volume 删除 nexus-prod_redis-data 数据卷(不可恢复)
|
||||
-h, --help 显示帮助
|
||||
|
||||
说明:
|
||||
- 不卸载宿主机 redis-server / 1Panel 应用商店 Redis
|
||||
- 卸载后请在宿主机安装 Redis,安装向导步骤 2 须能 ping 通
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--purge-volume) PURGE_VOLUME=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
removed=0
|
||||
|
||||
stop_rm_container() {
|
||||
local name="$1"
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
|
||||
info "停止并删除容器: $name"
|
||||
docker stop "$name" >/dev/null 2>&1 || true
|
||||
docker rm "$name" >/dev/null 2>&1 || true
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# 常见 Compose 命名
|
||||
for pattern in nexus-prod-redis-1 nexus-prod-redis nexus-redis-1; do
|
||||
stop_rm_container "$pattern"
|
||||
done
|
||||
|
||||
# 名称含 nexus 且含 redis 的容器
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*redis|redis.*nexus' || true)
|
||||
|
||||
# 若旧 compose 仍定义 redis 服务
|
||||
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]]; then
|
||||
if grep -q '^ redis:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
|
||||
warn "compose 仍含 redis 服务,尝试 compose rm..."
|
||||
local_env=()
|
||||
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
|
||||
prof="${NEXUS_ROOT}/docker/.host-profile"
|
||||
if [[ -f "$prof" ]]; then
|
||||
p="$(tr -d '\r\n' <"$prof")"
|
||||
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
|
||||
fi
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop redis 2>/dev/null || true)
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f redis 2>/dev/null || true)
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$PURGE_VOLUME" == true ]]; then
|
||||
while IFS= read -r vol; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
info "删除数据卷: $vol"
|
||||
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol(可能被占用)"
|
||||
done < <(docker volume ls -q | grep -iE 'nexus.*redis|redis.*nexus' || true)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$removed" -gt 0 ]]; then
|
||||
info "Compose Redis 容器已卸载。"
|
||||
else
|
||||
info "未发现 Nexus Compose Redis 容器(可能已删除)。"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "下一步:1Panel 应用商店安装 Redis → nx update 写入 NEXUS_1PANEL_REDIS_HOST"
|
||||
echo " 向导 Redis URL:redis://:密码@1Panel-redis-xxxx:6379/0(无 root 用户)"
|
||||
echo " 诊断:bash deploy/test-1panel-redis.sh"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus 6.0 — 一键更新(拉代码 + 重建镜像 + 重启)
|
||||
#
|
||||
# 已在 /opt/nexus:
|
||||
# bash deploy/update.sh
|
||||
# bash deploy/update.sh --no-cache # 强制无缓存重建(entrypoint/Dockerfile 变更后)
|
||||
#
|
||||
# 远程一条命令(推荐在仓库目录执行,管道模式会回退 /opt/nexus):
|
||||
# cd /opt/nexus && bash deploy/update.sh
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
_resolve_nexus_root() {
|
||||
local src="${BASH_SOURCE[0]:-}"
|
||||
if [[ -n "$src" && "$src" != /dev/fd/* && "$src" != "-" && -f "$src" ]]; then
|
||||
cd "$(dirname "$(readlink -f "$src")")/.." && pwd
|
||||
return
|
||||
fi
|
||||
if [[ -d /opt/nexus ]]; then
|
||||
echo "警告: 通过管道执行无法解析脚本路径,已回退 /opt/nexus;推荐: cd /opt/nexus && bash deploy/update.sh" >&2
|
||||
echo /opt/nexus
|
||||
return
|
||||
fi
|
||||
echo "错误: 无法定位 Nexus 根目录;请 cd /opt/nexus && bash deploy/update.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ROOT="$(_resolve_nexus_root)"
|
||||
|
||||
NEXUS_ROOT="$ROOT" bash "$ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||
exec bash "$ROOT/deploy/nexus-1panel.sh" upgrade "$@"
|
||||
Regular → Executable
+49
-55
@@ -1,16 +1,17 @@
|
||||
#!/bin/bash
|
||||
# ================================================================
|
||||
# Nexus 6.0 — Upgrade Script
|
||||
#!/usr/bin/env bash
|
||||
# Nexus 6.0 — Upgrade Script(自动识别 Docker / Supervisor)
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash upgrade.sh # auto-detect deploy dir
|
||||
# sudo bash upgrade.sh --deploy-dir /opt/nexus # specify deploy dir
|
||||
# sudo bash deploy/upgrade.sh
|
||||
# sudo bash deploy/upgrade.sh --deploy-dir /opt/nexus
|
||||
#
|
||||
# Safe: git pull + pip install + restart. Database is NOT touched.
|
||||
# ================================================================
|
||||
|
||||
# Docker 1Panel:委托 deploy/nexus-1panel.sh upgrade(拉代码 + 备份 + 重建镜像)
|
||||
# Supervisor 裸机:git pull + venv pip + supervisorctl restart
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./detect_deploy_runtime.sh
|
||||
source "${SCRIPT_DIR}/detect_deploy_runtime.sh"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
@@ -21,98 +22,90 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
DEPLOY_DIR=""
|
||||
EXTRA_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
case "$1" in
|
||||
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
|
||||
--no-backup|--require-backup|--no-cache|--prune|--check|--dry-run)
|
||||
EXTRA_ARGS+=("$1"); shift ;;
|
||||
-h|--help)
|
||||
echo "Usage: sudo bash upgrade.sh [--deploy-dir PATH]"
|
||||
exit 0 ;;
|
||||
*) shift ;;
|
||||
cat <<'EOF'
|
||||
用法: sudo bash deploy/upgrade.sh [--deploy-dir PATH] [nexus-1panel 升级选项]
|
||||
|
||||
Docker 环境自动走 nexus-1panel.sh upgrade;Supervisor 环境走 venv + supervisorctl。
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*) EXTRA_ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Auto-detect deploy directory
|
||||
if [ -z "$DEPLOY_DIR" ]; then
|
||||
# Check common locations
|
||||
for d in /opt/nexus /www/wwwroot/*/; do
|
||||
if [ -d "$d/server" ] && [ -d "$d/web" ]; then
|
||||
DEPLOY_DIR="$d"
|
||||
break
|
||||
fi
|
||||
done
|
||||
# Try from Supervisor config
|
||||
if [ -z "$DEPLOY_DIR" ]; then
|
||||
DEPLOY_DIR=$(supervisorctl status nexus 2>/dev/null | grep -oP 'cwd=\K[^ ,]+' || true)
|
||||
fi
|
||||
if [[ -z "$DEPLOY_DIR" ]]; then
|
||||
DEPLOY_DIR="$(resolve_nexus_root 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$DEPLOY_DIR" ] || [ ! -d "$DEPLOY_DIR/server" ]; then
|
||||
error "Cannot find Nexus installation. Use --deploy-dir to specify."
|
||||
if [[ -z "$DEPLOY_DIR" || ! -d "$DEPLOY_DIR/server" ]]; then
|
||||
error "找不到 Nexus 安装目录,请使用 --deploy-dir 指定"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUNTIME="$(detect_nexus_runtime "$DEPLOY_DIR")"
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
info "检测到 Docker Compose 运行时,委托 nexus-1panel.sh upgrade"
|
||||
exec env NEXUS_ROOT="$DEPLOY_DIR" bash "$DEPLOY_DIR/deploy/nexus-1panel.sh" upgrade "${EXTRA_ARGS[@]}"
|
||||
fi
|
||||
|
||||
VENV_DIR="$DEPLOY_DIR/venv"
|
||||
ENV_FILE="$DEPLOY_DIR/.env"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Nexus 6.0 Upgrade"
|
||||
echo " Nexus 6.0 Upgrade (Supervisor)"
|
||||
echo "=========================================="
|
||||
echo " Deploy: $DEPLOY_DIR"
|
||||
echo ""
|
||||
|
||||
# 1. Pull latest code
|
||||
info "Step 1/4: Updating source code..."
|
||||
if [ -d "$DEPLOY_DIR/.git" ]; then
|
||||
if [[ -d "$DEPLOY_DIR/.git" ]]; then
|
||||
cd "$DEPLOY_DIR"
|
||||
OLD_REV=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||
OLD_REV="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
git pull --ff-only 2>/dev/null || {
|
||||
error "git pull failed. You may have local changes."
|
||||
error "Resolve conflicts manually: cd $DEPLOY_DIR && git status"
|
||||
error "git pull failed. Resolve conflicts: cd $DEPLOY_DIR && git status"
|
||||
exit 1
|
||||
}
|
||||
NEW_REV=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||
if [ "$OLD_REV" = "$NEW_REV" ]; then
|
||||
NEW_REV="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
if [[ "$OLD_REV" == "$NEW_REV" ]]; then
|
||||
info " Already up to date (no new commits)"
|
||||
else
|
||||
info " Updated: ${OLD_REV:0:8}..${NEW_REV:0:8}"
|
||||
fi
|
||||
else
|
||||
warn " Not a git repo. Skip git pull (update files manually)"
|
||||
warn " Not a git repo. Skip git pull"
|
||||
fi
|
||||
|
||||
# 2. Update Python dependencies
|
||||
info "Step 2/4: Updating Python dependencies..."
|
||||
if [ -d "$VENV_DIR/bin" ]; then
|
||||
if [[ -d "$VENV_DIR/bin" ]]; then
|
||||
"$VENV_DIR/bin/pip" install -q --upgrade pip
|
||||
"$VENV_DIR/bin/pip" install -q -r "$DEPLOY_DIR/requirements.txt" 2>/dev/null || {
|
||||
warn " pip install had some warnings, but continuing..."
|
||||
warn " pip install had warnings, continuing..."
|
||||
}
|
||||
else
|
||||
error " venv not found at $VENV_DIR. Run install.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Restart application
|
||||
info "Step 3/4: Restarting Nexus..."
|
||||
supervisorctl restart nexus >/dev/null 2>&1 || {
|
||||
warn " supervisorctl restart failed. Try manually: supervisorctl restart nexus"
|
||||
warn " supervisorctl restart failed. Try: supervisorctl restart nexus"
|
||||
}
|
||||
|
||||
# 4. Health check
|
||||
info "Step 4/4: Verifying health..."
|
||||
|
||||
# Read port from .env or default
|
||||
PORT=8600
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
PORT_FROM_ENV=$(grep '^NEXUS_PORT=' "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || true)
|
||||
[ -n "$PORT_FROM_ENV" ] && PORT="$PORT_FROM_ENV"
|
||||
fi
|
||||
|
||||
PORT="$(nexus_publish_port_for "$DEPLOY_DIR")"
|
||||
HEALTH_OK=false
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
||||
for _ in $(seq 1 15); do
|
||||
if curl -sf "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
|
||||
HEALTH_OK=true
|
||||
break
|
||||
fi
|
||||
@@ -120,11 +113,12 @@ for i in $(seq 1 15); do
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [ "$HEALTH_OK" = true ]; then
|
||||
if [[ "$HEALTH_OK" == true ]]; then
|
||||
echo -e " ${GREEN}Upgrade complete! Nexus is healthy.${NC}"
|
||||
suggest_ops_cron "$DEPLOY_DIR"
|
||||
else
|
||||
echo -e " ${YELLOW}Health check pending. Verify manually:${NC}"
|
||||
echo " curl http://127.0.0.1:$PORT/health"
|
||||
echo " curl http://127.0.0.1:${PORT}/health"
|
||||
echo " supervisorctl status nexus"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify 1Panel + Docker install wizard readiness (run on the VPS as root).
|
||||
# See: deploy/README-1panel.md
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
ENV_PROD="${NEXUS_ROOT}/docker/.env.prod"
|
||||
COMPOSE_FILE="${NEXUS_ROOT}/docker/docker-compose.prod.yml"
|
||||
COMPOSE_1PANEL="${NEXUS_ROOT}/docker/docker-compose.1panel.yml"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
pass() { echo -e "${GREEN}[PASS]${NC} $*"; }
|
||||
fail() { echo -e "${RED}[FAIL]${NC} $*"; FAILURES=$((FAILURES + 1)); }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; WARNS=$((WARNS + 1)); }
|
||||
info() { echo -e " $*"; }
|
||||
|
||||
FAILURES=0
|
||||
WARNS=0
|
||||
INSTALL_WIZARD_ARCHIVED=false
|
||||
PUBLISH_PORT=8600
|
||||
|
||||
require_root() {
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
fail "请用 root 运行(1Panel 终端默认 root)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_nexus_container() {
|
||||
NEXUS_CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$NEXUS_CONTAINER" ]]; then
|
||||
fail "未找到运行中的 Nexus 容器"
|
||||
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
|
||||
return 1
|
||||
fi
|
||||
pass "Nexus 容器: $NEXUS_CONTAINER"
|
||||
}
|
||||
|
||||
check_repo() {
|
||||
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
|
||||
fail "仓库不存在: $NEXUS_ROOT"
|
||||
return 1
|
||||
fi
|
||||
local sha msg
|
||||
sha="$(git -C "$NEXUS_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
msg="$(git -C "$NEXUS_ROOT" log -1 --oneline 2>/dev/null || true)"
|
||||
pass "Git: $msg"
|
||||
if git -C "$NEXUS_ROOT" merge-base --is-ancestor b25d079 HEAD 2>/dev/null; then
|
||||
pass "已包含 b25d079(1Panel 容器名 sync 修复)"
|
||||
else
|
||||
warn "早于 b25d079 — 步骤 3 可能仍预填 host.docker.internal"
|
||||
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
|
||||
fi
|
||||
if git -C "$NEXUS_ROOT" merge-base --is-ancestor 3b2856f HEAD 2>/dev/null; then
|
||||
pass "已包含 3b2856f+(Redis :pass@ 自动解析,无 root)"
|
||||
else
|
||||
warn "早于 3b2856f — Redis 认证可能仍误用 root"
|
||||
info "修复: cd $NEXUS_ROOT && git pull && bash deploy/sync-install-wizard-to-container.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
check_env_prod() {
|
||||
if [[ ! -f "$ENV_PROD" ]]; then
|
||||
fail "缺少 $ENV_PROD"
|
||||
info "修复: bash $NEXUS_ROOT/deploy/install-nexus-fresh.sh --skip-clone"
|
||||
return 1
|
||||
fi
|
||||
pass "docker/.env.prod 存在"
|
||||
PUBLISH_PORT="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
PUBLISH_PORT="${PUBLISH_PORT:-8600}"
|
||||
pass "NEXUS_PUBLISH_PORT=$PUBLISH_PORT"
|
||||
if grep -qE '^NEXUS_INSTALL_WIZARD_PENDING=1' "$ENV_PROD"; then
|
||||
pass "NEXUS_INSTALL_WIZARD_PENDING=1(安装模式)"
|
||||
else
|
||||
warn "NEXUS_INSTALL_WIZARD_PENDING≠1 — entrypoint 可能已写 /app/.env"
|
||||
fi
|
||||
local db redis
|
||||
db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$db" ]]; then
|
||||
pass "NEXUS_1PANEL_DB_HOST=$db"
|
||||
else
|
||||
fail "未设置 NEXUS_1PANEL_DB_HOST"
|
||||
info "修复: bash $NEXUS_ROOT/deploy/detect-1panel-services.sh && nx update"
|
||||
fi
|
||||
if [[ -n "$redis" ]]; then
|
||||
pass "NEXUS_1PANEL_REDIS_HOST=$redis"
|
||||
local rurl
|
||||
rurl="$(grep -E '^NEXUS_REDIS_URL=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ "$rurl" == *"$redis"* ]]; then
|
||||
pass "NEXUS_REDIS_URL 已指向 Redis 容器"
|
||||
else
|
||||
warn "NEXUS_REDIS_URL 未含 $redis — nx update 会同步"
|
||||
fi
|
||||
else
|
||||
fail "未设置 NEXUS_1PANEL_REDIS_HOST"
|
||||
info "修复: 在 1Panel 应用商店安装 Redis 后执行 nx update"
|
||||
fi
|
||||
}
|
||||
|
||||
check_1panel_network() {
|
||||
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
fail "1panel-network 不存在"
|
||||
info "修复: 在 1Panel 应用商店安装 MySQL + Redis(会自动创建网络)"
|
||||
return 1
|
||||
fi
|
||||
pass "1panel-network 存在"
|
||||
if [[ -n "${NEXUS_CONTAINER:-}" ]]; then
|
||||
if docker inspect "$NEXUS_CONTAINER" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' \
|
||||
| grep -q '1panel-network'; then
|
||||
pass "Nexus 已接入 1panel-network"
|
||||
else
|
||||
fail "Nexus 未接入 1panel-network"
|
||||
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
|
||||
fi
|
||||
fi
|
||||
if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then
|
||||
info "探测结果:"
|
||||
"$NEXUS_ROOT/deploy/detect-1panel-services.sh" | sed 's/^/ /' || true
|
||||
fi
|
||||
}
|
||||
|
||||
probe_from_container() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
local db redis
|
||||
db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$db" ]]; then
|
||||
if docker exec "$NEXUS_CONTAINER" python3 -c "
|
||||
import socket
|
||||
s=socket.socket(); s.settimeout(3); s.connect(('$db', 3306)); s.close()
|
||||
" 2>/dev/null; then
|
||||
pass "容器内 TCP → MySQL $db:3306"
|
||||
else
|
||||
fail "容器内无法连接 MySQL $db:3306"
|
||||
info "检查 MySQL 容器是否运行、Nexus 是否在 1panel-network"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$redis" ]]; then
|
||||
if docker exec "$NEXUS_CONTAINER" python3 -c "
|
||||
import socket
|
||||
s=socket.socket(); s.settimeout(3); s.connect(('$redis', 6379)); s.close()
|
||||
" 2>/dev/null; then
|
||||
pass "容器内 TCP → Redis $redis:6379"
|
||||
else
|
||||
fail "容器内无法连接 Redis $redis:6379"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_app_env_redis_url() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
if ! docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
local rurl
|
||||
rurl="$(docker exec "$NEXUS_CONTAINER" grep -E '^NEXUS_REDIS_URL=' /app/.env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
|
||||
if [[ -z "$rurl" ]]; then
|
||||
warn "/app/.env 无 NEXUS_REDIS_URL"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$rurl" == *"://root:"* ]]; then
|
||||
fail "NEXUS_REDIS_URL 含 root 用户(1Panel Redis 无 root 账号)"
|
||||
info "修复: redis://:密码@1Panel-redis-xxxx:6379/0 — 见 deploy/test-1panel-redis.sh"
|
||||
elif [[ "$rurl" == *"1Panel-redis"* ]] || [[ "$rurl" == *"://:"* ]]; then
|
||||
pass "NEXUS_REDIS_URL 格式合理(容器名或 :pass@)"
|
||||
else
|
||||
warn "NEXUS_REDIS_URL 可能未指向 1Panel Redis: $rurl"
|
||||
fi
|
||||
}
|
||||
|
||||
check_install_mode() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
if docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then
|
||||
warn "/app/.env 已存在 — 安装向导可能已进入步骤 3+ 或误写 env"
|
||||
check_app_env_redis_url
|
||||
info "若需重来: 删卷 nexus-state 中 .env 并设 NEXUS_INSTALL_WIZARD_PENDING=1 后 nx update"
|
||||
else
|
||||
pass "/app/.env 不存在(安装模式 OK)"
|
||||
fi
|
||||
if docker exec "$NEXUS_CONTAINER" test -f /app/.install_locked 2>/dev/null \
|
||||
|| docker exec "$NEXUS_CONTAINER" test -f /var/lib/nexus/.install_locked 2>/dev/null \
|
||||
|| docker exec "$NEXUS_CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null; then
|
||||
pass "安装向导已锁定/已归档 — 应访问 /app/ 登录"
|
||||
INSTALL_WIZARD_ARCHIVED=true
|
||||
fi
|
||||
local hosts_json
|
||||
hosts_json="$(docker exec "$NEXUS_CONTAINER" cat /app/web/data/1panel-hosts.json 2>/dev/null || true)"
|
||||
if [[ -n "$hosts_json" ]]; then
|
||||
pass "1panel-hosts.json: $hosts_json"
|
||||
else
|
||||
warn "无 1panel-hosts.json(依赖容器环境变量,nx update 会写入)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_ops_cron() {
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" != true ]]; then
|
||||
return 0
|
||||
fi
|
||||
if ! command -v crontab >/dev/null 2>&1; then
|
||||
warn "未安装 cron/crontab — Layer 3 宿主机巡检不可用"
|
||||
info "修复: sudo bash $NEXUS_ROOT/deploy/install_ops_cron.sh"
|
||||
return 0
|
||||
fi
|
||||
local cron
|
||||
cron="$(crontab -l 2>/dev/null || true)"
|
||||
if echo "$cron" | grep -q 'nexus-health-monitor\|deploy/health_monitor.sh'; then
|
||||
pass "Crontab 含 health_monitor"
|
||||
else
|
||||
warn "Crontab 未配置 health_monitor(Layer 3 巡检)"
|
||||
info "修复: sudo nx cron"
|
||||
fi
|
||||
if echo "$cron" | grep -q 'nexus-mysql-backup\|deploy/db_backup.sh'; then
|
||||
pass "Crontab 含 db_backup"
|
||||
else
|
||||
warn "Crontab 未配置 db_backup(每日 MySQL 备份)"
|
||||
info "修复: sudo nx cron"
|
||||
fi
|
||||
}
|
||||
|
||||
check_mysql_backup() {
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" != true ]]; then
|
||||
return 0
|
||||
fi
|
||||
local dump_sh="${NEXUS_ROOT}/deploy/mysql_dump_to_file.sh"
|
||||
if [[ -f "$dump_sh" ]]; then
|
||||
pass "mysql_dump_to_file.sh 存在"
|
||||
else
|
||||
warn "缺少 mysql_dump_to_file.sh — 升级前无法备份 MySQL"
|
||||
info "修复: cd $NEXUS_ROOT && git pull && nx update"
|
||||
return 0
|
||||
fi
|
||||
if [[ -d /var/backups/nexus ]] && ls /var/backups/nexus/nexus_*.sql.gz >/dev/null 2>&1; then
|
||||
local latest
|
||||
latest="$(ls -t /var/backups/nexus/nexus_*.sql.gz 2>/dev/null | head -1)"
|
||||
pass "已有 MySQL 备份: $(basename "$latest")"
|
||||
else
|
||||
warn "尚无 MySQL 备份文件(/var/backups/nexus)"
|
||||
info "可手动: sudo bash $NEXUS_ROOT/deploy/db_backup.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
check_http_local() {
|
||||
local health code
|
||||
health="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/health" 2>/dev/null || true)"
|
||||
if [[ "$health" == "ok" ]]; then
|
||||
pass "/health → ok"
|
||||
else
|
||||
fail "/health 非 ok(当前: ${health:-无响应})"
|
||||
info "修复: docker logs --tail=80 $NEXUS_CONTAINER"
|
||||
return 1
|
||||
fi
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
|
||||
if [[ "$code" == "404" ]]; then
|
||||
pass "/app/install.html 已归档 → HTTP 404"
|
||||
else
|
||||
fail "/app/install.html 应已归档但 HTTP $code"
|
||||
info "修复: nx update(会执行 archive_install_wizard_in_container)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [[ "$code" == "200" ]]; then
|
||||
pass "/app/install.html → HTTP 200"
|
||||
else
|
||||
fail "/app/install.html → HTTP $code"
|
||||
info "修复: bash $NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
check_install_api() {
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
|
||||
pass "安装 API 已关闭(向导已锁定,/api/install/* 返回 403 为预期)"
|
||||
return 0
|
||||
fi
|
||||
local status env_json
|
||||
status="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/api/install/status" 2>/dev/null || true)"
|
||||
if [[ -n "$status" ]]; then
|
||||
pass "/api/install/status → $status"
|
||||
if echo "$status" | grep -q '"installed":true'; then
|
||||
warn "installed=true — 向导已完成,请访问 /app/ 登录"
|
||||
fi
|
||||
else
|
||||
fail "/api/install/status 无响应"
|
||||
fi
|
||||
env_json="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/api/install/env-check" 2>/dev/null || true)"
|
||||
if [[ -z "$env_json" ]]; then
|
||||
fail "/api/install/env-check 无响应"
|
||||
return 1
|
||||
fi
|
||||
if echo "$env_json" | grep -q 'docker_defaults'; then
|
||||
local db_host
|
||||
db_host="$(echo "$env_json" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print((d.get('docker_defaults') or {}).get('db_host',''))
|
||||
" 2>/dev/null || true)"
|
||||
if [[ -n "$db_host" && "$db_host" != "host.docker.internal" ]]; then
|
||||
pass "步骤 3 预填 db_host=$db_host"
|
||||
elif [[ "$db_host" == "host.docker.internal" ]]; then
|
||||
fail "步骤 3 仍预填 host.docker.internal"
|
||||
info "修复: nx update --no-cache(需 b25d079+)"
|
||||
fi
|
||||
else
|
||||
warn "env-check 无 docker_defaults(非 Docker 模式?)"
|
||||
fi
|
||||
if echo "$env_json" | grep -q '1Panel-mysql'; then
|
||||
pass "env-check MySQL 探测含 1Panel 容器名"
|
||||
elif echo "$env_json" | grep -q 'host.docker.internal'; then
|
||||
warn "env-check MySQL 仍指向 host.docker.internal"
|
||||
fi
|
||||
}
|
||||
|
||||
check_container_image_features() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
local host_sha py_ok html_file html_ok host_root_ok
|
||||
host_sha="$(git -C "$NEXUS_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
pass "宿主机 Git: $host_sha"
|
||||
|
||||
if docker exec "$NEXUS_CONTAINER" grep -q '_host_deploy_root' /app/server/api/install.py 2>/dev/null; then
|
||||
pass "容器 install.py 含 host_deploy_root(round6+)"
|
||||
py_ok=1
|
||||
else
|
||||
warn "容器 install.py 较旧 — 需 nx update 重建镜像"
|
||||
fi
|
||||
|
||||
html_file="/app/web/app/install.html"
|
||||
if docker exec "$NEXUS_CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null; then
|
||||
html_file="/app/web/app/install.html.bak"
|
||||
fi
|
||||
if docker exec "$NEXUS_CONTAINER" grep -q 'install_ops_cron' "$html_file" 2>/dev/null; then
|
||||
pass "容器 install.html 含 Layer 3 cron 指引(round7+)"
|
||||
html_ok=1
|
||||
else
|
||||
warn "容器 install.html 较旧 — 需 nx update 重建镜像"
|
||||
fi
|
||||
|
||||
host_root_ok="$(docker exec "$NEXUS_CONTAINER" printenv NEXUS_HOST_ROOT 2>/dev/null || true)"
|
||||
if [[ -n "$host_root_ok" ]]; then
|
||||
pass "NEXUS_HOST_ROOT=$host_root_ok"
|
||||
else
|
||||
warn "容器未注入 NEXUS_HOST_ROOT — nx update 重建后可修复"
|
||||
fi
|
||||
|
||||
if [[ -n "${py_ok:-}" && -n "${html_ok:-}" ]]; then
|
||||
info "镜像与宿主机代码特性对齐(重建后验收)"
|
||||
else
|
||||
info "修复: cd $NEXUS_ROOT && sudo nx update"
|
||||
fi
|
||||
}
|
||||
|
||||
check_openresty_hint() {
|
||||
local domain code
|
||||
domain="$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo api.synaglobal.vip)"
|
||||
code="$(curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 5 "https://${domain}/health" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "200" ]] && curl -sk "https://${domain}/health" 2>/dev/null | grep -q '^ok$'; then
|
||||
pass "HTTPS https://${domain}/health → ok"
|
||||
else
|
||||
warn "HTTPS https://${domain}/health → HTTP $code(外网可能未配 OpenResty 反代)"
|
||||
info "1Panel → 网站 → 反代 http://127.0.0.1:8600"
|
||||
info "参考: $NEXUS_ROOT/deploy/1panel/openresty-nexus.conf.example"
|
||||
fi
|
||||
}
|
||||
|
||||
print_next_steps() {
|
||||
local domain
|
||||
domain="$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo '你的域名')"
|
||||
echo ""
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
|
||||
echo "=========================================="
|
||||
echo " 已安装 — 日常运维"
|
||||
echo "=========================================="
|
||||
echo " 登录: https://${domain}/app/"
|
||||
echo " 更新: cd $NEXUS_ROOT && nx update"
|
||||
echo " 巡检 cron: sudo nx cron"
|
||||
echo " 手动备份: sudo bash $NEXUS_ROOT/deploy/db_backup.sh"
|
||||
echo " 验收: bash $NEXUS_ROOT/deploy/verify-1panel-install-wizard.sh"
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
echo "=========================================="
|
||||
echo " 安装向导后续步骤"
|
||||
echo "=========================================="
|
||||
echo "1. 1Panel 数据库 → 建库 nexus、用户 nexus(仅授权 nexus 库)"
|
||||
echo "2. 浏览器打开: https://${domain}/app/install.html"
|
||||
echo "3. 步骤 2:MySQL/Redis TCP 应显示 ✓(容器名:端口)"
|
||||
echo "4. 步骤 3:确认 db_host/redis_host 为 1Panel-mysql/redis-xxxx,填 MySQL 密码"
|
||||
echo "5. 步骤 4:连接检测全绿 → 创建 admin"
|
||||
echo "6. 步骤 5:锁定安装"
|
||||
echo ""
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
echo "先修复上方 [FAIL],再执行:"
|
||||
echo " cd $NEXUS_ROOT && nx update --no-cache"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
require_root
|
||||
echo "Nexus 1Panel 安装向导验收 — $(date -Iseconds)"
|
||||
echo "NEXUS_ROOT=$NEXUS_ROOT"
|
||||
echo ""
|
||||
|
||||
check_repo || true
|
||||
check_env_prod || true
|
||||
resolve_nexus_container || true
|
||||
check_1panel_network || true
|
||||
probe_from_container || true
|
||||
check_install_mode || true
|
||||
check_http_local || true
|
||||
check_install_api || true
|
||||
check_ops_cron || true
|
||||
check_mysql_backup || true
|
||||
check_container_image_features || true
|
||||
check_openresty_hint || true
|
||||
print_next_steps
|
||||
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
echo ""
|
||||
fail "合计 $FAILURES 项未通过"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
|
||||
pass "验收通过 — 安装已完成,请访问 /app/ 登录"
|
||||
if [[ "$WARNS" -gt 0 ]]; then
|
||||
warn "有 ${WARNS} 项 [WARN](cron/备份等建议项,不阻断登录)"
|
||||
fi
|
||||
else
|
||||
pass "验收通过 — 可打开安装向导完成步骤 1–5"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,9 +1,6 @@
|
||||
# Production Compose env — copy to docker/.env.prod (never commit)
|
||||
# Fresh install: bash deploy/install-nexus-fresh.sh (auto-generates unique secrets per host)
|
||||
# Migration: bash deploy/nexus-1panel.sh install --from-env /path/to/old/.env
|
||||
|
||||
MYSQL_ROOT_PASSWORD=change_me_root_strong
|
||||
MYSQL_PASSWORD=change_me_nexus_strong
|
||||
# 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=
|
||||
@@ -17,13 +14,26 @@ NEXUS_INSTALL_WIZARD_PENDING=0
|
||||
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
|
||||
NEXUS_API_BASE_URL=https://api.synaglobal.vip
|
||||
|
||||
# 2 vCPU / 8 GiB host (matches docker/mysql-prod-2c8g.cnf + compose limits)
|
||||
# Nexus container resource profile (docker/profiles/*.env)
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
|
||||
# Redis 不在 Docker 栈内 — 请在宿主机或 1Panel 应用商店自行安装
|
||||
# Nexus 容器通过 host.docker.internal 访问宿主机 Redis(见 docker-compose.prod.yml)
|
||||
# 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 — production probe / E2E only (host .env.prod; NOT for API runtime; never commit values)
|
||||
# NEXUS_TEST_ADMIN_PASSWORD — must match admin login password
|
||||
# NEXUS_PROBE_CLIENT_IP — optional; IP in login allowlist for origin probe (auto-read from DB if unset)
|
||||
# NEXUS_TEST_ADMIN_PASSWORD=
|
||||
# NEXUS_PROBE_CLIENT_IP=
|
||||
|
||||
# Optional
|
||||
# NEXUS_PUBLISH_PORT=8600
|
||||
|
||||
+8
-7
@@ -41,15 +41,16 @@ docker compose build --no-cache nexus
|
||||
|
||||
## 服务
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| nexus | 8600 | FastAPI + 静态 `/app/` |
|
||||
| mysql | 3306 | 数据库 `nexus`(Compose 内) |
|
||||
| redis | — | **不在 Compose 内**;宿主机/1Panel 自建,默认 `host.docker.internal:6379` |
|
||||
| 环境 | Compose 服务 | 说明 |
|
||||
|------|-------------|------|
|
||||
| 本地开发 `docker-compose.yml` | nexus + mysql + redis | 一键本地联调 |
|
||||
| 生产 `docker-compose.prod.yml` | **仅 nexus** | MySQL/Redis 宿主机或 1Panel 自建 |
|
||||
|
||||
生产 1Panel:`docker/.env.prod` 可含 `NEXUS_REDIS_URL`(供 `nx update` 探测同步),**不经 Compose 注入容器**;运行时 Redis/MySQL URL 以安装向导写入的 `/app/.env`(`nexus-state` 卷)为准。MySQL `NEXUS_DATABASE_URL` 仅由向导步骤 3 写入。
|
||||
|
||||
## 生产注意
|
||||
|
||||
- 修改 `MYSQL_*` 与 `NEXUS_*` 为强密码,勿使用 `generate_env.py` 默认值上生产。
|
||||
- 修改 `NEXUS_*` 为强密码;MySQL 使用自建库强密码。
|
||||
- 建议前置 Nginx/TLS,仅暴露 443。
|
||||
- 多实例部署需共享 MySQL、Redis;后台任务由 Redis primary lock 保证单实例执行。
|
||||
|
||||
@@ -62,7 +63,7 @@ docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod up
|
||||
|
||||
- 镜像:`Dockerfile.prod`(含 `npm ci` + `vite build` + **`web/app/install.html` 安装向导**)
|
||||
- 仅绑定 `127.0.0.1:8600`;OpenResty 反代见 `deploy/1panel/openresty-nexus.conf.example`
|
||||
- **2C8G 调优**:`docker/mysql-prod-2c8g.cnf`(buffer pool 1G、`max_connections=120`)+ 容器 `mem_limit` + `NEXUS_DB_POOL_SIZE=30`
|
||||
- **2C8G 调优**:`docker/profiles/2c8g.env`(`NEXUS_MEM_LIMIT` + `NEXUS_DB_POOL_SIZE=30`);外置 MySQL 可参考 `docker/mysql-prod-2c8g.cnf` 手工调优
|
||||
- 完整迁移步骤:`docs/design/plans/2026-06-04-1panel-docker-production.md`
|
||||
- 一键安装:`bash deploy/install-1panel-docker.sh`
|
||||
- 一键升级:`bash deploy/upgrade-1panel-docker.sh`(`--check` 仅查更新)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# 1Panel overlay — join Nexus to 1panel-network (App Store MySQL/Redis use this network).
|
||||
# Applied automatically when `docker network inspect 1panel-network` succeeds (deploy/nexus-1panel.sh).
|
||||
|
||||
services:
|
||||
nexus:
|
||||
networks:
|
||||
- 1panel-network
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
name: 1panel-network
|
||||
@@ -1,9 +1,8 @@
|
||||
# Nexus 6.0 — production Compose (1Panel / bare Docker host)
|
||||
# Resource profile: docker/profiles/{1c4g,2c8g,4c16g}.env — see deploy/nexus-1panel.sh
|
||||
# MySQL / Redis 不在 Compose 内 — 请在宿主机或 1Panel 自行安装
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/nexus-1panel.sh install --profile 2c8g
|
||||
# # or manually:
|
||||
# docker compose -f docker/docker-compose.prod.yml \
|
||||
# --env-file docker/.env.prod --env-file docker/profiles/2c8g.env up -d --build
|
||||
#
|
||||
@@ -12,51 +11,25 @@
|
||||
name: nexus-prod
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: nexus
|
||||
MYSQL_USER: nexus
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?set MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ./${MYSQL_CNF_FILE:-mysql-prod-2c8g.cnf}:/etc/mysql/conf.d/99-nexus-prod.cnf:ro
|
||||
mem_limit: ${MYSQL_MEM_LIMIT:-2g}
|
||||
cpus: ${MYSQL_CPUS:-1.5}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
start_period: 40s
|
||||
networks:
|
||||
- nexus-internal
|
||||
|
||||
nexus:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.prod
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "127.0.0.1:${NEXUS_PUBLISH_PORT:-8600}:8600"
|
||||
environment:
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_PORT: "3306"
|
||||
NEXUS_PERSIST_DIR: /var/lib/nexus
|
||||
NEXUS_DOCKER_WRITE_ENV: "1"
|
||||
NEXUS_HOST: "0.0.0.0"
|
||||
NEXUS_PORT: "8600"
|
||||
NEXUS_DEPLOY_PATH: /app
|
||||
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD}@mysql:3306/nexus
|
||||
# Redis 不在 Compose 内 — 请在宿主机/1Panel 自建,默认连宿主机 6379
|
||||
NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://host.docker.internal:6379/0}
|
||||
NEXUS_HOST_ROOT: ${NEXUS_HOST_ROOT:-/opt/nexus}
|
||||
# Redis URL 仅来自 /app/.env(安装向导 + entrypoint export_app_env),勿在此注入以免覆盖密码
|
||||
NEXUS_1PANEL_DB_HOST: ${NEXUS_1PANEL_DB_HOST:-}
|
||||
NEXUS_1PANEL_REDIS_HOST: ${NEXUS_1PANEL_REDIS_HOST:-}
|
||||
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:?set NEXUS_CORS_ORIGINS}
|
||||
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:?set NEXUS_SECRET_KEY}
|
||||
NEXUS_API_KEY: ${NEXUS_API_KEY:?set NEXUS_API_KEY}
|
||||
@@ -71,10 +44,10 @@ services:
|
||||
mem_limit: ${NEXUS_MEM_LIMIT:-2g}
|
||||
cpus: ${NEXUS_CPUS:-1.5}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://127.0.0.1:8600/health"]
|
||||
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
networks:
|
||||
- nexus-internal
|
||||
@@ -84,6 +57,5 @@ networks:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
nexus-state:
|
||||
nexus-web-data:
|
||||
|
||||
+91
-20
@@ -24,7 +24,8 @@ wait_tcp_optional() {
|
||||
host="$1"
|
||||
port="$2"
|
||||
tries="${3:-15}"
|
||||
echo "entrypoint: waiting for external Redis ${host}:${port} (max ${tries} tries)..."
|
||||
label="${4:-service}"
|
||||
echo "entrypoint: waiting for ${label} ${host}:${port} (max ${tries} tries)..."
|
||||
n=0
|
||||
while [ "$n" -lt "$tries" ]; do
|
||||
if python3 -c "
|
||||
@@ -34,22 +35,79 @@ s.settimeout(2)
|
||||
s.connect(('${host}', ${port}))
|
||||
s.close()
|
||||
" 2>/dev/null; then
|
||||
echo "entrypoint: Redis ready at ${host}:${port}"
|
||||
echo "entrypoint: ${label} ready at ${host}:${port}"
|
||||
return 0
|
||||
fi
|
||||
n=$((n + 1))
|
||||
sleep 2
|
||||
done
|
||||
echo "entrypoint: warn — Redis not reachable at ${host}:${port}; install on host or set NEXUS_REDIS_URL"
|
||||
echo "entrypoint: warn — ${label} not reachable at ${host}:${port}; install on host or set URL in .env"
|
||||
return 0
|
||||
}
|
||||
|
||||
restore_persisted_env() {
|
||||
if [ -f "${PERSIST_ENV}" ]; then
|
||||
cp "${PERSIST_ENV}" /app/.env
|
||||
chmod 600 /app/.env
|
||||
echo "entrypoint: restored .env from ${PERSIST_ENV}"
|
||||
restore_persisted_install_lock() {
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
PERSIST_LOCK="${PERSIST_DIR}/.install_locked"
|
||||
if [ -f "${PERSIST_LOCK}" ] && [ ! -f /app/.install_locked ]; then
|
||||
cp "${PERSIST_LOCK}" /app/.install_locked
|
||||
chmod 600 /app/.install_locked
|
||||
echo "entrypoint: restored .install_locked from ${PERSIST_LOCK}"
|
||||
fi
|
||||
}
|
||||
|
||||
archive_install_wizard_if_locked() {
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" = "1" ]; then
|
||||
rm -f /app/.install_locked "${PERSIST_DIR}/.install_locked" 2>/dev/null || true
|
||||
if [ -f /app/web/app/install.html.bak ] && [ ! -f /app/web/app/install.html ]; then
|
||||
mv /app/web/app/install.html.bak /app/web/app/install.html
|
||||
echo "entrypoint: restored install.html from .bak (wizard pending)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f /app/.install_locked ] && [ ! -f "${PERSIST_DIR}/.install_locked" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -f /app/web/app/install.html.bak ]; then
|
||||
if [ -f /app/web/app/install.html ]; then
|
||||
rm -f /app/web/app/install.html
|
||||
echo "entrypoint: removed install.html (wizard archived as install.html.bak)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [ -f /app/web/app/install.html ]; then
|
||||
mv /app/web/app/install.html /app/web/app/install.html.bak
|
||||
echo "entrypoint: archived install.html → install.html.bak"
|
||||
fi
|
||||
}
|
||||
|
||||
restore_persisted_env() {
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "${PERSIST_ENV}" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! grep -q '^NEXUS_DATABASE_URL=' "${PERSIST_ENV}" 2>/dev/null; then
|
||||
echo "entrypoint: warn — ${PERSIST_ENV} missing NEXUS_DATABASE_URL; skip restore (complete install wizard)"
|
||||
return 0
|
||||
fi
|
||||
cp "${PERSIST_ENV}" /app/.env
|
||||
chmod 600 /app/.env
|
||||
echo "entrypoint: restored .env from ${PERSIST_ENV}"
|
||||
}
|
||||
|
||||
# Compose may inject placeholder NEXUS_REDIS_URL (no password). Wizard .env wins.
|
||||
export_app_env() {
|
||||
if [ ! -f /app/.env ]; then
|
||||
return 0
|
||||
fi
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. /app/.env
|
||||
set +a
|
||||
echo "entrypoint: exported NEXUS_* from /app/.env (overrides Compose placeholders)"
|
||||
}
|
||||
|
||||
write_env_from_environment() {
|
||||
@@ -66,6 +124,10 @@ write_env_from_environment() {
|
||||
echo "entrypoint: install wizard pending — skip writing /app/.env"
|
||||
return 0
|
||||
fi
|
||||
if [ -z "${NEXUS_DATABASE_URL:-}" ]; then
|
||||
echo "entrypoint: skip writing /app/.env — NEXUS_DATABASE_URL not set (use /app/install.html)"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "${PERSIST_DIR}"
|
||||
{
|
||||
echo "# Generated by docker/entrypoint.sh — do not commit"
|
||||
@@ -76,29 +138,38 @@ write_env_from_environment() {
|
||||
echo "entrypoint: wrote .env from container environment"
|
||||
}
|
||||
|
||||
persist_env_on_exit() {
|
||||
persist_state_on_exit() {
|
||||
if [ -f /app/.env ]; then
|
||||
mkdir -p "${PERSIST_DIR}"
|
||||
cp /app/.env "${PERSIST_ENV}"
|
||||
chmod 600 "${PERSIST_ENV}" 2>/dev/null || true
|
||||
fi
|
||||
if [ -f /app/.install_locked ]; then
|
||||
mkdir -p "${PERSIST_DIR}"
|
||||
cp /app/.install_locked "${PERSIST_DIR}/.install_locked"
|
||||
chmod 600 "${PERSIST_DIR}/.install_locked" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap persist_env_on_exit EXIT INT TERM
|
||||
trap persist_state_on_exit EXIT INT TERM
|
||||
|
||||
MYSQL_HOST="${MYSQL_HOST:-mysql}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
|
||||
wait_tcp "${MYSQL_HOST}" "${MYSQL_PORT}"
|
||||
|
||||
# Redis 由用户自行安装在宿主机/1Panel;安装向导阶段不阻塞启动
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_REDIS_WAIT:-0}" != "1" ]; then
|
||||
REDIS_HOST="${REDIS_HOST:-host.docker.internal}"
|
||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||
wait_tcp_optional "${REDIS_HOST}" "${REDIS_PORT}" 15
|
||||
# MySQL / Redis 均由用户自行安装在宿主机;安装向导阶段不阻塞启动
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_MYSQL_WAIT:-0}" != "1" ]; then
|
||||
MYSQL_HOST="${NEXUS_1PANEL_DB_HOST:-${MYSQL_HOST:-host.docker.internal}}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
wait_tcp_optional "${MYSQL_HOST}" "${MYSQL_PORT}" 30 "MySQL"
|
||||
fi
|
||||
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_REDIS_WAIT:-0}" != "1" ]; then
|
||||
REDIS_HOST="${NEXUS_1PANEL_REDIS_HOST:-${REDIS_HOST:-host.docker.internal}}"
|
||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||
wait_tcp_optional "${REDIS_HOST}" "${REDIS_PORT}" 15 "Redis"
|
||||
fi
|
||||
|
||||
restore_persisted_install_lock
|
||||
archive_install_wizard_if_locked
|
||||
restore_persisted_env
|
||||
write_env_from_environment
|
||||
export_app_env
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# Host profile: 1 vCPU / 4 GiB (test / small VPS)
|
||||
MYSQL_CNF_FILE=mysql-prod-1c4g.cnf
|
||||
MYSQL_MEM_LIMIT=1g
|
||||
MYSQL_CPUS=1
|
||||
# Host profile: 1 vCPU / 4 GiB
|
||||
NEXUS_MEM_LIMIT=1g
|
||||
NEXUS_CPUS=1
|
||||
NEXUS_DB_POOL_SIZE=15
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# Host profile: 2 vCPU / 8 GiB — default production
|
||||
MYSQL_CNF_FILE=mysql-prod-2c8g.cnf
|
||||
MYSQL_MEM_LIMIT=2g
|
||||
MYSQL_CPUS=1.5
|
||||
NEXUS_MEM_LIMIT=2g
|
||||
NEXUS_CPUS=1.5
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Host profile: 4 vCPU / 16 GiB
|
||||
MYSQL_CNF_FILE=mysql-prod-4c16g.cnf
|
||||
MYSQL_MEM_LIMIT=6g
|
||||
MYSQL_CPUS=3
|
||||
NEXUS_MEM_LIMIT=4g
|
||||
NEXUS_CPUS=3
|
||||
NEXUS_DB_POOL_SIZE=60
|
||||
NEXUS_DB_MAX_OVERFLOW=40
|
||||
NEXUS_DB_POOL_SIZE=50
|
||||
NEXUS_DB_MAX_OVERFLOW=30
|
||||
|
||||
+17
-4
@@ -17,7 +17,9 @@
|
||||
| [project/risk-acceptance-single-operator.md](project/risk-acceptance-single-operator.md) | 单人运维风险接受 |
|
||||
| [project/script-execution.md](project/script-execution.md) | 脚本执行 |
|
||||
| [project/alert-push-policy.md](project/alert-push-policy.md) | 告警策略 |
|
||||
| [project/deploy.md](project/deploy.md) | 部署补充 |
|
||||
| [project/deploy.md](project/deploy.md) | 部署补充(宝塔/裸机) |
|
||||
| [project/nexus-1panel-operations-knowledge.md](project/nexus-1panel-operations-knowledge.md) | **1Panel + Docker 运维知识 SSOT** |
|
||||
| [project/nexus-god-menu-audit-summary.md](project/nexus-god-menu-audit-summary.md) | **nx 上帝菜单巡检归档**(2026-06-06 round1–10) |
|
||||
| [project/production-verification-checklist.md](project/production-verification-checklist.md) | L5 清单 |
|
||||
|
||||
根目录 [AGENTS.md](../AGENTS.md) · [CLAUDE.md](../CLAUDE.md) · [.cursorrules](../.cursorrules) 为薄入口。
|
||||
@@ -47,7 +49,18 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. 设计与标准
|
||||
## 4. 测试(全站用例 SSOT)
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [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. 设计与标准
|
||||
|
||||
| 目录 | 说明 |
|
||||
|------|------|
|
||||
@@ -58,13 +71,13 @@
|
||||
|
||||
---
|
||||
|
||||
## 5. 归档区
|
||||
## 6. 归档区
|
||||
|
||||
**[archive/README.md](archive/README.md)** — 2026-06 审计合并卷、旧 handoff、agentmemory、2026-05 审查报告。
|
||||
|
||||
---
|
||||
|
||||
## 6. 工具脚本
|
||||
## 7. 工具脚本
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# 审计 — 2026-06-07 BL-06 Agent per-server key 强制
|
||||
|
||||
## 范围
|
||||
|
||||
移除 Agent 认证 global `API_KEY` legacy 回退;涉及 3 个后端文件 + 1 个新测试文件。
|
||||
|
||||
| 文件 | 行数 | Read 范围 |
|
||||
|------|------|-----------|
|
||||
| `server/api/agent.py` | 379 | L40-93(认证函数) |
|
||||
| `server/api/servers.py` | 2056 | L619-636, L1625-1640(安装链路) |
|
||||
| `server/application/services/script_service.py` | 735 | L44-50(回调 key) |
|
||||
| `tests/test_agent_per_server_key.py` | 88 | 全文 |
|
||||
| `tests/load_test.py` | 202 | 顶部 docstring |
|
||||
| `tests/quick_load.py` | 87 | 顶部 docstring |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | Agent 认证 fail-closed | FIXED 移除 global 回退;无 per-server key → False |
|
||||
| H2 | 安装链路静默降级 | FIXED 无 key 明确 400/批量错误项 |
|
||||
| H3 | 长任务回调 key 泄露面 | FIXED 仅用 per-server key,无 global 回退 |
|
||||
| H4 | compare_digest 时序安全 | SAFE 保留 `secrets.compare_digest` |
|
||||
| H5 | DB 查服异常 | SAFE 保持 fail-closed return False |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | FIXED | `agent.py:60-82` 仅 `agent_api_key` 匹配 |
|
||||
| H2 | FIXED | `servers.py:627-633`, `servers.py:1635-1637` |
|
||||
| H3 | FIXED | `script_service.py:44-50` ValueError |
|
||||
| H4 | SAFE | `agent.py:82` compare_digest |
|
||||
| H5 | SAFE | `agent.py:68-76` except → False |
|
||||
|
||||
## 入口表(agent.py 受影响路由)
|
||||
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| POST | `/api/agent/heartbeat` | X-API-Key + per-server `agent_api_key` |
|
||||
| POST | `/api/agent/script-callback` | 同上 + job secret |
|
||||
|
||||
## 巡检补修(同日)
|
||||
|
||||
| 项 | 原问题 | 修复 |
|
||||
|----|--------|------|
|
||||
| H6 | `receive_heartbeat` 认证后再次 `get_server`(重复查库) | `_verify_server_api_key` 返回 `Optional[Server]`,handler 复用 |
|
||||
| H7 | 验 key 通过后 `unknown_server` discarded 分支为死代码 | 删除;不存在/无 key/错 key 统一 401 |
|
||||
| H8 | `script-callback` docstring 仍写「per-server or global」 | 改为仅 per-server |
|
||||
| H9 | 无回归测试约束 `get_server` 调用次数 | `test_verify_server_key_single_get_server_per_auth_check` + `assert_called_once_with` |
|
||||
|
||||
## 生产只读探测(2026-06-07)
|
||||
|
||||
| 探测 | 结果 |
|
||||
|------|------|
|
||||
| `GET /health` | 200 |
|
||||
| `POST /api/agent/heartbeat` 假 key | 401 |
|
||||
| `security_probe_external.sh` heartbeat/WS | PASS |
|
||||
|
||||
注:BL-06 完整语义(global key 不可冒充)需部署本地改动后生产才生效;当前生产假 key 已 401。
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] ruff 零错误(改动文件)
|
||||
- [x] import server.main
|
||||
- [x] pytest `test_agent_per_server_key.py` 9 passed(含 `assert_called_once`)
|
||||
- [x] changelog ≥10 行
|
||||
- [x] 无静默吞错;无 global key 降级
|
||||
- [x] 心跳路径单次 `get_server`(巡检补修)
|
||||
@@ -0,0 +1,67 @@
|
||||
# 审计 — 2026-06-07 ce-code-review 跟进 + BL-07 探测
|
||||
|
||||
## 范围
|
||||
|
||||
本批次 `git diff HEAD~1` 含 **BL-06**(已提交)与 **ce-code-review 跟进**(工作区)。BL-06 细读见 `2026-06-07-bl06-agent-per-server-key.md`。
|
||||
|
||||
| 文件 | 变更要点 |
|
||||
|------|----------|
|
||||
| `server/api/servers.py` | BL-06:安装链路无 global key 回退 |
|
||||
| `server/application/services/script_service.py` | BL-06:回调仅 per-server key |
|
||||
| `tests/test_agent_per_server_key.py` | BL-06:认证单元测试 |
|
||||
| `tests/load_test.py` | BL-06:文档注释 |
|
||||
| `tests/quick_load.py` | BL-06:文档注释 |
|
||||
| `web/agent/agent.py` | 心跳 401 退出循环(对齐 BL-06 中心端) |
|
||||
| `server/api/install.py` | 路由级 lock Depends;归档先于写 lock;归档失败 fail-closed |
|
||||
| `.cursor/rules/nexus-security.mdc` | Agent 认证矩阵:仅 per-server key |
|
||||
| `scripts/security_probe_external.sh` | curl `shift` 修复;`resolve_ws_python`;WS 403 → PASS |
|
||||
| `docs/changelog/2026-06-07-deploy-ssh-secrets.md` | 本机 SSH secrets 文档 |
|
||||
| `docs/changelog/2026-06-07-probe-curl-parse-fix.md` | 探测脚本 curl/WS 修复 |
|
||||
| `docs/reports/2026-06-07-bl07-deploy-verification.md` | 生产部署验证报告 |
|
||||
| `tests/test_install_locked_404.py` | create-admin / test-credentials 锁定 404;router Depends 断言 |
|
||||
| `tests/test_security_unit.py` | `_reject_if_locked` 期望 404 |
|
||||
| `deploy/install-nx-cli.sh` | CRLF → LF(门控 pre-flight) |
|
||||
| `deploy/update.sh` | CRLF → LF |
|
||||
| `deploy/gate_log.jsonl` | 门控运行记录(自动生成) |
|
||||
| `deploy/deploy-production.sh` | source `nexus-1panel.secrets.sh` 加载 SSH |
|
||||
| `deploy/nexus-1panel.secrets.sh.example` | SSH 变量占位 |
|
||||
| `docs/project/linux-dev-paths.md` | 本机 SSH/部署 SSOT |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 边端 agent 401 不重试 | FIXED `agent.py` 401 → return |
|
||||
| H2 | 锁定 install 统一 404 | FIXED `install.py` router `Depends(_raise_not_found_if_locked)` |
|
||||
| H3 | 归档失败仍 lock 成功 | FIXED 归档先于 lock;OSError → RuntimeError → 500 |
|
||||
| H4 | 安全 SSOT 与 BL-06 漂移 | FIXED `nexus-security.mdc` |
|
||||
| H5 | 外网 WS 403 误判反代 | FIXED 探测脚本 PASS + 报告澄清 |
|
||||
| H6 | 探测 curl `shift 3` 误报 FAIL | FIXED `probe()` shift 2/3 + 独立 code 文件 |
|
||||
| H7 | WS 探测 SKIP(无 websockets) | FIXED `resolve_ws_python()` 自动 venv |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | FIXED | `web/agent/agent.py` `elif resp.status_code == 401: return` |
|
||||
| H2 | FIXED | `install.py` `APIRouter(..., dependencies=[Depends(...)])` |
|
||||
| H3 | FIXED | `_finalize_install_lock` 先 `_archive_install_wizard_html` |
|
||||
| H4 | FIXED | `nexus-security.mdc` 认证矩阵 |
|
||||
| H5 | FIXED | `security_probe_external.sh` WS 403 PASS |
|
||||
| H6 | FIXED | `probe()` `shift 2` + `code_file` |
|
||||
| H7 | FIXED | `resolve_ws_python()` |
|
||||
|
||||
## 验证
|
||||
|
||||
| 项 | 结果 |
|
||||
|----|------|
|
||||
| `bash scripts/local_verify.sh` | 全绿(smoke 47 + test_api 26/26 + ruff) |
|
||||
| `pytest tests/test_install_locked_404.py` | 11 passed |
|
||||
| `deploy/pre_deploy_check.sh` | 待本审计入库后重跑 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] 无静默吞错(归档失败 error + 500)
|
||||
- [x] changelog `2026-06-07-code-review-followups.md`
|
||||
- [x] 本地 L2b `local_verify` 证据
|
||||
- [x] 涉及文件列入本审计范围表
|
||||
@@ -0,0 +1,51 @@
|
||||
# 审计 — 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
|
||||
@@ -0,0 +1,17 @@
|
||||
# 审计 — 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✓
|
||||
@@ -0,0 +1,44 @@
|
||||
# 审计 — Layer 3 宿主机巡检 Telegram 打通
|
||||
|
||||
## 范围
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `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/api/settings.py` | `set_setting` 钩子;`GET/POST ops-patrol/*` |
|
||||
| `deploy/health_monitor.sh` | `load_telegram_from_db` + flock 修复 + **Telegram 引号剥离** |
|
||||
| `deploy/deploy-production.sh` | 部署成功自动 `install_ops_cron.sh` |
|
||||
| `deploy/nexus-1panel.sh` | `sync_env_prod_to_volume` 含 Telegram 键 |
|
||||
| `frontend/src/pages/SettingsPage.vue` | Layer 3 巡检状态与同步 UI |
|
||||
| `frontend/src/types/api.ts` | `OpsPatrol*` 类型 |
|
||||
| `tests/test_ops_patrol_sync.py` | 7 用例 |
|
||||
| `docs/changelog/2026-06-07-ops-patrol-layer3-telegram.md` | changelog |
|
||||
| `docs/design/plans/2026-06-07-ops-patrol-layer3-telegram.md` | 技术说明 |
|
||||
| `deploy/gate_log.jsonl` | 门控记录(自动生成) |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | Layer-3 与设置页 Telegram 脱节 | FIXED — 保存时 sync + DB 回退 |
|
||||
| H2 | `.env` 写入破坏不可变键 | SAFE — 仅 upsert `NEXUS_TELEGRAM_*` |
|
||||
| H3 | 宿主机 cron 未安装 | FIXED — deploy 自动 install_ops_cron |
|
||||
| H4 | env 同步失败静默 | SAFE — 记录 warning;DB 回退保底 |
|
||||
| H5 | 敏感 Token 泄露至日志 | SAFE — 无 log token;审计仅写布尔标志 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H5 | FIXED / SAFE | 代码 + pytest 7/7 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest `test_ops_patrol_sync.py` + `test_settings_telegram.py` 13 passed
|
||||
- [x] changelog / audit / plan
|
||||
- [x] 无 SECRET_KEY/API_KEY/ENCRYPTION_KEY/DATABASE_URL 变更
|
||||
|
||||
## 验证
|
||||
|
||||
`.venv/bin/pytest tests/test_ops_patrol_sync.py tests/test_settings_telegram.py -q`
|
||||
@@ -0,0 +1,28 @@
|
||||
# 审计 — 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 重启验证
|
||||
@@ -0,0 +1,35 @@
|
||||
# 审计 — 设置页 IP 白名单订阅恢复
|
||||
|
||||
## 范围
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/pages/SettingsPage.vue` | 订阅 URL、解析预览、节点展示、正确 save payload |
|
||||
| `frontend/src/types/api.ts` | `SubscriptionParseResponse`、`IpAllowlistSaveRequest` |
|
||||
| `deploy/install_ops_cron.sh` | 同日前序 commit(cron bash 调用) |
|
||||
| `deploy/gate_log.jsonl` | 门控记录 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 后端 API 未删 | PASS — `parse-subscription` / `IpAllowlistSaveRequest` 仍在 |
|
||||
| H2 | SSRF 仍由后端拦截 | PASS — 未改 `settings.py` |
|
||||
| H3 | 错误 payload 修复 | FIXED — 不再 POST `{ ips }` |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | `server/api/settings.py` |
|
||||
| H2 | PASS | 解析仍走服务端 |
|
||||
| H3 | FIXED | `saveAllowlist` payload |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] vite build 通过
|
||||
- [x] changelog / audit
|
||||
|
||||
## 验证
|
||||
|
||||
设置页 → 登录 IP 白名单 → 订阅 URL + 解析预览 + 保存
|
||||
@@ -0,0 +1,43 @@
|
||||
# 审计 — 设置页 Telegram Chat ID 检测恢复
|
||||
|
||||
## 范围
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/pages/SettingsPage.vue` | 恢复「检测 Chat ID」按钮与选择对话框 |
|
||||
| `frontend/src/types/api.ts` | `TelegramChatsResponse` 类型 |
|
||||
| `deploy/install_ops_cron.sh` | crontab 改用 `bash` 调用(避免 +x 丢失) |
|
||||
| `deploy/deploy-production.sh` | `PORT` 缺省 8600(同日前序 commit) |
|
||||
| `deploy/gate_log.jsonl` | 门控记录(自动生成) |
|
||||
|
||||
## 结论
|
||||
|
||||
- 后端 `GET /settings/telegram/chats` 未删,仅 Vue 设置页遗漏 UI
|
||||
- 无 API 契约变更;检测前若 Token 草稿存在则先 PUT 保存
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | Chat ID 检测 API 仍存在 | PASS — `GET /settings/telegram/chats` 未改 |
|
||||
| H2 | Vue 设置页功能对等 | FIXED — 恢复检测按钮与对话框 |
|
||||
| H3 | Token 未保存时检测 | FIXED — 检测前自动 PUT token 草稿 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | `server/api/settings.py` `telegram_get_chats` |
|
||||
| H2 | FIXED | `SettingsPage.vue` `detectTelegramChats` |
|
||||
| H3 | FIXED | 检测前 `http.put telegram_bot_token` |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] 前端 vite build 通过
|
||||
- [x] changelog / audit 已写
|
||||
- [x] 无 API 破坏性变更
|
||||
|
||||
## 验证
|
||||
|
||||
- `cd frontend && npx vite build` 通过
|
||||
- 设置页:Bot 收消息后检测 → 列表 → 选中写入 Chat ID
|
||||
@@ -0,0 +1,37 @@
|
||||
# 审计 — 设置页 Telegram 全流程修复
|
||||
|
||||
## 范围
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/pages/SettingsPage.vue` | persist/save/test/detect、8 NOTIFY 开关、reveal 免密 |
|
||||
| `frontend/src/types/api.ts` | `NOTIFY_TOGGLE_ITEMS` |
|
||||
| `server/api/settings.py` | Webhook 清除、错误详情、reveal 免密 |
|
||||
| `tests/test_settings_telegram.py` | 6 用例 |
|
||||
| `deploy/gate_log.jsonl` | 门控记录(自动生成) |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 测试/检测前持久化 Token+Chat ID | FIXED — `persistTelegramConfig` |
|
||||
| H2 | getUpdates Webhook 冲突 | FIXED — `deleteWebhook` before poll |
|
||||
| H3 | Telegram API 错误可见 | FIXED — `ok:false` / sendMessage description |
|
||||
| H4 | 8 项 NOTIFY 开关 UI | FIXED — Vue v-switch + PUT |
|
||||
| H5 | reveal 免密(用户确认) | FIXED — JWT + 审计保留 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H5 | FIXED | 代码 + pytest 6/6 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest `test_settings_telegram.py` 6 passed
|
||||
- [x] changelog / audit
|
||||
- [x] 无不可变 key 变更
|
||||
|
||||
## 验证
|
||||
|
||||
`.venv/bin/pytest tests/test_settings_telegram.py -q`
|
||||
@@ -0,0 +1,29 @@
|
||||
# 审计 — 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;待生产快速添加验证
|
||||
@@ -0,0 +1,36 @@
|
||||
# 审计 — 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`
|
||||
@@ -0,0 +1,26 @@
|
||||
# 审计 — 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 后)
|
||||
@@ -0,0 +1,111 @@
|
||||
# 审计 — 北京时间展示统一 + 资源恢复 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)
|
||||
@@ -0,0 +1,25 @@
|
||||
# 审计 — 仪表盘服务器列表不显示
|
||||
|
||||
**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] 仪表盘服务器表格可见
|
||||
- [ ] 生产浏览器验收
|
||||
@@ -0,0 +1,46 @@
|
||||
# 审计 — 生产部署批次(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] 单测与门控
|
||||
@@ -0,0 +1,27 @@
|
||||
# 审计 — 全量对齐部署(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 标记存在
|
||||
@@ -0,0 +1,28 @@
|
||||
# 审计 — 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`
|
||||
@@ -0,0 +1,22 @@
|
||||
# 审计 — 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` 已更新
|
||||
@@ -0,0 +1,46 @@
|
||||
# 审计 — 登录 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 骨架屏
|
||||
@@ -0,0 +1,33 @@
|
||||
# 审计 — 资源恢复 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 + 浏览器验收
|
||||
@@ -0,0 +1,31 @@
|
||||
# 审计 — 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 + 浏览器验收
|
||||
@@ -0,0 +1,33 @@
|
||||
# 审计 — 页面自动刷新(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/`
|
||||
@@ -0,0 +1,41 @@
|
||||
# 审计 — 推送后台化 + 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 按钮
|
||||
- [ ] 生产部署与健康检查
|
||||
@@ -0,0 +1,41 @@
|
||||
# 审计 — 调度周期「当天」文案与单次 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
|
||||
@@ -0,0 +1,74 @@
|
||||
# 审计 — 调度执行周期可视化(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`)。
|
||||
@@ -0,0 +1,102 @@
|
||||
# 审计 — 调度表单批次 + 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/` 静态资源即可 |
|
||||
@@ -0,0 +1,72 @@
|
||||
# 审计 — 脚本执行全局进度与看板(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(与告警一致)
|
||||
@@ -0,0 +1,39 @@
|
||||
# 审计 — 脚本执行 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
|
||||
@@ -0,0 +1,35 @@
|
||||
# 审计 — 脚本看板名称 / 顶栏提示(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 + 本审计
|
||||
@@ -0,0 +1,25 @@
|
||||
# 审计 — 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] 单测
|
||||
@@ -0,0 +1,27 @@
|
||||
# 审计 — 服务器展开详情对比度 + 同步日志 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] 同步日志标题与条数
|
||||
- [ ] 生产浏览器验收
|
||||
@@ -0,0 +1,112 @@
|
||||
# 审计 — 服务器批量任务 · 执行记录 · 凭据合并(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*` 等变更属并行会话,部署时须拆分或一并补审计。
|
||||
@@ -0,0 +1,123 @@
|
||||
# 审计 — 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,可部署
|
||||
@@ -0,0 +1,25 @@
|
||||
# 审计 — 服务器列表实时字段排序
|
||||
|
||||
**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] 单测
|
||||
@@ -0,0 +1,43 @@
|
||||
# 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 超时合并
|
||||
- [ ] 「复制失败清单」剪贴板内容正确
|
||||
@@ -0,0 +1,26 @@
|
||||
# 审计 — 未设路径服务器批量管理对齐主表
|
||||
|
||||
**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
|
||||
@@ -0,0 +1,30 @@
|
||||
# 审计 — 服务器未设路径区块 + 行内详情
|
||||
|
||||
**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 三态
|
||||
@@ -0,0 +1,25 @@
|
||||
# 审计 — 设置表单校验 + 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] 单测
|
||||
@@ -0,0 +1,22 @@
|
||||
# 审计 — 设置页连接池保存
|
||||
|
||||
**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
|
||||
@@ -0,0 +1,29 @@
|
||||
# 审计 — 终端命令栏深链修复(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] 本审计列出上述文件
|
||||
@@ -0,0 +1,23 @@
|
||||
# 审计 — 终端命令栏与断开遮罩
|
||||
|
||||
**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 区
|
||||
@@ -0,0 +1,29 @@
|
||||
# 审计 — 终端快捷栏断开按钮(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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user