Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8363746cf6 | |||
| b49dad8bda | |||
| 279259092d | |||
| 74149e0d90 | |||
| 1933f0af6e |
@@ -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`
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"plugins": {
|
||||
"compound-engineering": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -29,13 +29,25 @@ FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA(15 页)· 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/`
|
||||
|
||||
## 进度条(改代码时输出)
|
||||
|
||||
|
||||
+40
-29
@@ -1,29 +1,40 @@
|
||||
.git
|
||||
.gitignore
|
||||
.claude
|
||||
.cursor
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
**/.pytest_cache
|
||||
**/.mypy_cache
|
||||
**/.ruff_cache
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
node_modules
|
||||
|
||||
.env
|
||||
docker/.env
|
||||
docker/runtime
|
||||
|
||||
tests/
|
||||
docs/
|
||||
standards/
|
||||
*.md
|
||||
!docker/README.md
|
||||
|
||||
web/uploads
|
||||
backups/
|
||||
deploy/gate_log.jsonl
|
||||
.git
|
||||
.gitignore
|
||||
.claude
|
||||
.cursor
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
**/.pytest_cache
|
||||
**/.mypy_cache
|
||||
**/.ruff_cache
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
frontend/node_modules
|
||||
frontend-v2/node_modules
|
||||
frontend/dist
|
||||
node_modules
|
||||
.venv
|
||||
.venv-*
|
||||
venv
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
.playwright-mcp
|
||||
2025.2
|
||||
=2025.2
|
||||
*.bak-
|
||||
|
||||
.env
|
||||
docker/.env
|
||||
docker/runtime
|
||||
tmp/
|
||||
|
||||
tests/
|
||||
docs/
|
||||
standards/
|
||||
*.md
|
||||
!docker/README.md
|
||||
|
||||
web/uploads
|
||||
backups/
|
||||
deploy/gate_log.jsonl
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
# Nexus Environment Configuration
|
||||
# Copy this file to .env and modify values for your deployment.
|
||||
# Or run the web installer at /app/install.html to auto-generate this file.
|
||||
#
|
||||
# Generate secret keys: openssl rand -hex 32
|
||||
|
||||
# ── Brand ──
|
||||
NEXUS_SYSTEM_NAME=Nexus
|
||||
NEXUS_SYSTEM_TITLE=Nexus — 服务器运维管理平台
|
||||
|
||||
# ── Server ──
|
||||
NEXUS_HOST=0.0.0.0
|
||||
NEXUS_PORT=8600
|
||||
|
||||
# ── Deployment (auto-set by installer) ──
|
||||
NEXUS_DEPLOY_PATH=/opt/nexus
|
||||
# Comma-separated frontend origins for CORS
|
||||
NEXUS_CORS_ORIGINS=http://localhost:8600
|
||||
# Base URL for Agent heartbeat reporting
|
||||
NEXUS_API_BASE_URL=http://localhost:8600
|
||||
|
||||
# ── Database (MySQL, REQUIRED) ──
|
||||
NEXUS_DATABASE_URL=mysql+aiomysql://root:password@127.0.0.1:3306/nexus
|
||||
# Pool params: auto-calculated by installer from MySQL max_connections
|
||||
# Formula: pool_size = max(20, maxConn*0.4), overflow = max(20, maxConn*0.3)
|
||||
# Default below matches MySQL max_connections=400 (160+120=280 max)
|
||||
# install wizard will overwrite these with values tuned to your MySQL instance
|
||||
NEXUS_DB_POOL_SIZE=160
|
||||
NEXUS_DB_MAX_OVERFLOW=120
|
||||
|
||||
# ── Security (REQUIRED — generate with: openssl rand -hex 32) ──
|
||||
NEXUS_SECRET_KEY=change-me-use-openssl-rand-hex-32
|
||||
NEXUS_API_KEY=change-me-use-openssl-rand-hex-32
|
||||
NEXUS_ENCRYPTION_KEY=
|
||||
|
||||
# ── Redis (REQUIRED) ──
|
||||
NEXUS_REDIS_URL=redis://127.0.0.1:6379/0
|
||||
|
||||
# ── SSH ──
|
||||
NEXUS_SSH_STRICT_HOST_CHECKING=false
|
||||
|
||||
# ── Alert Thresholds (%) ──
|
||||
NEXUS_CPU_ALERT_THRESHOLD=80
|
||||
NEXUS_MEM_ALERT_THRESHOLD=80
|
||||
NEXUS_DISK_ALERT_THRESHOLD=80
|
||||
|
||||
# ── Health Check ──
|
||||
NEXUS_HEALTH_CHECK_INTERVAL=60
|
||||
|
||||
# ── Telegram Alerts (optional — configure in Settings UI) ──
|
||||
NEXUS_TELEGRAM_BOT_TOKEN=
|
||||
NEXUS_TELEGRAM_CHAT_ID=
|
||||
|
||||
# ── MySQL MCP (@yclenove/mysql-mcp-server — run: python scripts/sync_mysql_mcp_env.py) ──
|
||||
# Cursor 从项目根 .env 读取;与 NEXUS_DATABASE_URL 保持一致;默认只读
|
||||
# MYSQL_HOST=127.0.0.1
|
||||
# MYSQL_PORT=3306
|
||||
# MYSQL_USER=root
|
||||
# MYSQL_PASSWORD=password
|
||||
# MYSQL_DATABASE=nexus
|
||||
# MYSQL_READONLY=true # production / MCP read-only
|
||||
# MYSQL_READONLY=false # local dev: full DML/DDL via MCP
|
||||
# MYSQL_DATABASE_ALLOWLIST=nexus
|
||||
@@ -35,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
|
||||
|
||||
+90
-88
@@ -1,88 +1,90 @@
|
||||
# Nexus — Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
build/
|
||||
.eggs/
|
||||
|
||||
# Environment (NEVER commit — contains production credentials)
|
||||
.env
|
||||
.install_locked
|
||||
.install_state.json
|
||||
SECRETS.md
|
||||
.git-askpass-nexus.sh
|
||||
deploy/nexus-1panel.secrets.sh
|
||||
*.pem
|
||||
.env.*
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Agent memory (local MCP persistence, do not commit)
|
||||
.claude/mcp-memory.jsonl
|
||||
|
||||
# MCP tool data (local development artifacts)
|
||||
.megamemory/
|
||||
.playwright-mcp/
|
||||
frontend/test-results/
|
||||
tmp/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
nexus.log
|
||||
|
||||
# Node offline cache for WSL install (large tarball)
|
||||
.cache/
|
||||
|
||||
# Test
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
.coverage
|
||||
coverage.xml
|
||||
|
||||
# Node (for Tailwind/Alpine.js build)
|
||||
node_modules/
|
||||
web/css/tailwind-output.css
|
||||
|
||||
# Build output (Vite/Vuetify frontend)
|
||||
# SPA 构建产物纳入 Git,便于服务器 git pull 部署(见 deploy/deploy-frontend.sh)
|
||||
# 仍忽略本地临时文件
|
||||
web/app/assets/.tmp/
|
||||
# Legacy dist dir (no longer used)
|
||||
**/dist/
|
||||
|
||||
# Uploads
|
||||
web/uploads/
|
||||
|
||||
# Data (sensitive)
|
||||
web/data/config.php
|
||||
web/data/*.db
|
||||
web/data/1panel-hosts.json
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
# Docker local secrets
|
||||
docker/.env
|
||||
docker/.env.prod
|
||||
!docker/.env.example
|
||||
!docker/.env.prod.example
|
||||
scripts/npm-proxy.env
|
||||
|
||||
# Deploy
|
||||
deploy/*.key
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
# Nexus — Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
build/
|
||||
.eggs/
|
||||
|
||||
# Environment (NEVER commit — contains production credentials)
|
||||
.env
|
||||
.install_locked
|
||||
.install_state.json
|
||||
SECRETS.md
|
||||
.git-askpass-nexus.sh
|
||||
deploy/nexus-1panel.secrets.sh
|
||||
*.pem
|
||||
.env.*
|
||||
.venv/
|
||||
.venv-*/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Agent memory (local MCP persistence, do not commit)
|
||||
.claude/mcp-memory.jsonl
|
||||
|
||||
# MCP tool data (local development artifacts)
|
||||
.megamemory/
|
||||
.playwright-mcp/
|
||||
frontend/test-results/
|
||||
frontend/e2e/.auth/
|
||||
tmp/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
nexus.log
|
||||
|
||||
# Node offline cache for WSL install (large tarball)
|
||||
.cache/
|
||||
|
||||
# Test
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
.coverage
|
||||
coverage.xml
|
||||
|
||||
# Node (for Tailwind/Alpine.js build)
|
||||
node_modules/
|
||||
web/css/tailwind-output.css
|
||||
|
||||
# Build output (Vite/Vuetify frontend)
|
||||
# SPA 构建产物纳入 Git,便于服务器 git pull 部署(见 deploy/deploy-frontend.sh)
|
||||
# 仍忽略本地临时文件
|
||||
web/app/assets/.tmp/
|
||||
# Legacy dist dir (no longer used)
|
||||
**/dist/
|
||||
|
||||
# Uploads
|
||||
web/uploads/
|
||||
|
||||
# Data (sensitive)
|
||||
web/data/config.php
|
||||
web/data/*.db
|
||||
web/data/1panel-hosts.json
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
# Docker local secrets
|
||||
docker/.env
|
||||
docker/.env.prod
|
||||
!docker/.env.example
|
||||
!docker/.env.prod.example
|
||||
scripts/npm-proxy.env
|
||||
|
||||
# Deploy
|
||||
deploy/*.key
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: nexus-btpanel-review
|
||||
description: Use when reviewing or changing Nexus BT/宝塔 panel integration, one-click login, API bootstrap, session keepalive fixes, or BT credential handling.
|
||||
---
|
||||
|
||||
# Nexus BT Panel Review
|
||||
|
||||
Use for 宝塔/BT panel integration changes.
|
||||
|
||||
## Call chain to map
|
||||
|
||||
API one-click login -> btpanel_service -> BT API credential lookup/bootstrap -> SSH bootstrap if needed -> login URL/tmp_token -> panel session behavior.
|
||||
|
||||
## Required checks
|
||||
|
||||
- Do not log or return BT API keys, session cookies, temporary login tokens, SSH private keys, or passwords.
|
||||
- One-click login should fail closed for real authentication errors, but best-effort environment repair must not block login unless required for correctness.
|
||||
- Session keepalive/cleanup repair must be idempotent and throttled; do not restart or rewrite BT panel aggressively without explicit approval.
|
||||
- Respect existing BT panel services and ports. Do not occupy panel, nginx, redis, or mysql ports.
|
||||
- Old servers with existing BT API credentials must still receive the session-cleanup TTL check before login.
|
||||
- Record bootstrap state without storing secrets.
|
||||
|
||||
## Regression tests
|
||||
|
||||
Cover:
|
||||
|
||||
- Existing credentials still trigger throttled TTL check.
|
||||
- Missing SSH auth does not block login URL generation.
|
||||
- Failed repair is throttled and retried later.
|
||||
- Temporary login token TTL remains bounded.
|
||||
- No secret values appear in returned bootstrap/status payloads.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: nexus-security-review
|
||||
description: Use when reviewing or changing Nexus backend/API/security-sensitive code. Focuses on FastAPI admin APIs, SQLAlchemy data access, SSH operations, file manager boundaries, logging redaction, and regression tests.
|
||||
---
|
||||
|
||||
# Nexus Security Review
|
||||
|
||||
Use this skill before changing Nexus backend/API code or doing a security pass.
|
||||
|
||||
## Required workflow
|
||||
|
||||
1. Identify entrypoints: FastAPI router, service method, DB model/session, Redis/lock usage, SSH/BT panel call.
|
||||
2. Draw the request path in notes: API -> schema -> service -> DB/Redis/SSH/external.
|
||||
3. Check auth first: admin-only endpoints must depend on the current admin dependency; background/admin tools must still preserve audit logs.
|
||||
4. Validate every user-controlled value at the schema/API boundary and again before shell/path-sensitive sinks.
|
||||
5. For every fix, add pytest coverage for the dangerous case and the allowed case.
|
||||
6. Run focused tests, then full pytest when practical, then git diff --check.
|
||||
7. Update the Nexus Markdown report with changed files, tests, and residual risks.
|
||||
|
||||
## Nexus-specific rules
|
||||
|
||||
- Do not print secrets: passwords, SSH keys, tokens, cookies, API keys, .env values, BT panel credentials.
|
||||
- script_service is an intentional administrator remote shell. Do not report arbitrary shell execution there as a vulnerability. Review only auth, audit trail, timeout, output truncation, and redaction.
|
||||
- File manager APIs must not unexpectedly operate outside the target path/server chosen by the admin UI.
|
||||
- Any remote shell command must use shlex.quote; add -- where command options can be confused with filenames.
|
||||
- Archive extraction must list and validate members before extraction; reject absolute paths, .., backslashes, and special file types.
|
||||
- Error responses must not expose stack traces or secrets.
|
||||
- DB access should use SQLAlchemy parameters/query builder, not raw string interpolation.
|
||||
|
||||
## Review output
|
||||
|
||||
For each finding record:
|
||||
|
||||
- Severity: Critical/High/Medium/Low/Hardening
|
||||
- Call chain
|
||||
- Evidence file/function
|
||||
- Exploit or failure boundary
|
||||
- Fix summary
|
||||
- Test command/result
|
||||
- Commit hash if committed
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: nexus-ssh-safety
|
||||
description: Use when reviewing or editing Nexus code that executes commands on managed servers over SSH, including file manager, transfer, archive, agent install, and BT panel bootstrap flows.
|
||||
---
|
||||
|
||||
# Nexus SSH Safety
|
||||
|
||||
Use for any Nexus feature that builds shell commands or transfers files over SSH.
|
||||
|
||||
## Command construction rules
|
||||
|
||||
- Prefer argument arrays/local library APIs when available; otherwise quote every dynamic shell token with shlex.quote.
|
||||
- Insert -- before filename/path operands for commands that parse options:
|
||||
m, mv, cp, ar, zip, chmod, chown, etc.
|
||||
- Never concatenate unquoted user input into shell command strings.
|
||||
- Keep timeouts explicit for network/SSH work; avoid unbounded commands.
|
||||
- Truncate or sanitize command output before returning it to API clients.
|
||||
- Do not log secrets or full credential-bearing URLs.
|
||||
|
||||
## Path and archive rules
|
||||
|
||||
- Normalize paths with POSIX semantics for remote Linux paths.
|
||||
- Reject empty paths where dangerous, root/system paths for recursive destructive operations, and traversal where the feature expects a bounded directory.
|
||||
- Archive creation must protect member names beginning with -.
|
||||
- Archive extraction must validate member names and types before extraction.
|
||||
- Temporary files/directories must be unique and cleaned best-effort.
|
||||
|
||||
## script_service exception
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Collecting tzdata
|
||||
Downloading tzdata-2026.2-py2.py3-none-any.whl.metadata (1.4 kB)
|
||||
Downloading tzdata-2026.2-py2.py3-none-any.whl (349 kB)
|
||||
Installing collected packages: tzdata
|
||||
Successfully installed tzdata-2026.2
|
||||
@@ -19,6 +19,14 @@ cd frontend && npm run dev # :3000/app/
|
||||
|
||||
路径见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)。
|
||||
|
||||
## Git(仅本机快照)
|
||||
|
||||
- 本地 `git commit` 用于回滚;**不 `git push`**(除非你明确要求)。
|
||||
- 已移除 `origin` 跟踪,避免误推;原远程:`http://66.154.115.8:3000/admin/Nexus.git`(需恢复时说一声)。
|
||||
- `.env`、`SECRETS.md`、`*.pem` 已在 `.gitignore`,**勿提交**。
|
||||
- 改代码仍写 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)。
|
||||
- 用户说「提交快照 / commit」时执行。
|
||||
|
||||
## 强制约束
|
||||
|
||||
- 完美实现:`.cursor/rules/perfect-implementation.mdc`
|
||||
@@ -32,13 +40,57 @@ cd frontend && npm run dev # :3000/app/
|
||||
|
||||
## 部署
|
||||
|
||||
默认 **本机 rsync → SSH 生产机**(不 push Gitea):
|
||||
|
||||
```bash
|
||||
bash deploy/pre_deploy_check.sh
|
||||
bash deploy/deploy-production.sh # 需 SSH nexus + 用户批准
|
||||
bash deploy/deploy-production.sh # rsync + Docker upgrade --skip-git + 前端
|
||||
```
|
||||
|
||||
可选:`NEXUS_DEPLOY_VIA_GIT=1` 恢复远程 `git pull`。仅 `git push` 当你明确要求时。
|
||||
|
||||
详情见功能指南 §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 个文件 → 停止**。
|
||||
|
||||
+46
-33
@@ -1,33 +1,46 @@
|
||||
# Nexus 6.0 — API + 静态 SPA (web/app)
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
openssh-client \
|
||||
rsync \
|
||||
sshpass \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server/ ./server/
|
||||
COPY web/app/ ./web/app/
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
RUN chmod +x ./docker/entrypoint.sh
|
||||
|
||||
ENV PYTHONPATH=/app \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
NEXUS_HOST=0.0.0.0 \
|
||||
NEXUS_PORT=8600
|
||||
|
||||
EXPOSE 8600
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||||
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
||||
# Nexus 6.0 - API + static SPA (web/app)
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
ARG DEBIAN_MIRROR=https://mirrors.aliyun.com/debian
|
||||
ARG DEBIAN_SECURITY_MIRROR=https://mirrors.aliyun.com/debian-security
|
||||
ARG PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/
|
||||
ARG PIP_TRUSTED_HOST=mirrors.aliyun.com
|
||||
|
||||
RUN set -eux; \
|
||||
sed -i \
|
||||
-e "s|http://deb.debian.org/debian-security|${DEBIAN_SECURITY_MIRROR}|g" \
|
||||
-e "s|http://deb.debian.org/debian|${DEBIAN_MIRROR}|g" \
|
||||
/etc/apt/sources.list.d/debian.sources; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
openssh-client \
|
||||
rsync \
|
||||
sshpass; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --index-url "${PIP_INDEX_URL}" --trusted-host "${PIP_TRUSTED_HOST}" -r requirements.txt
|
||||
|
||||
COPY server/ ./server/
|
||||
COPY web/app/ ./web/app/
|
||||
COPY web/app-v2/ ./web/app-v2/
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
RUN chmod +x ./docker/entrypoint.sh
|
||||
|
||||
ENV PYTHONPATH=/app \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
NEXUS_HOST=0.0.0.0 \
|
||||
NEXUS_PORT=8600
|
||||
|
||||
EXPOSE 8600
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||||
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
||||
|
||||
|
||||
|
||||
+77
-64
@@ -1,64 +1,77 @@
|
||||
# Nexus 6.0 — production image (frontend build + API)
|
||||
# Build: docker compose -f docker/docker-compose.prod.yml build
|
||||
# See: docs/design/plans/2026-06-04-1panel-docker-production.md
|
||||
|
||||
# ── Stage 1: Vue/Vite SPA ──
|
||||
FROM node:20-bookworm-slim AS frontend
|
||||
WORKDIR /build/frontend
|
||||
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY frontend/ ./
|
||||
RUN npx vite build
|
||||
|
||||
# ── 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 \
|
||||
openssh-client \
|
||||
rsync \
|
||||
sshpass \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server/ ./server/
|
||||
COPY web/agent/ ./web/agent/
|
||||
COPY --from=frontend /build/web/app ./web/app/
|
||||
# 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 \
|
||||
web/app/vendor/theme.css \
|
||||
web/app/vendor/alpinejs.min.js \
|
||||
./web/app/vendor/
|
||||
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/vendor/alpinejs.min.js \
|
||||
&& test -f /app/web/app/vendor/tailwindcss-browser.js \
|
||||
&& test -f /app/web/app/vendor/theme.css \
|
||||
&& test -f /app/web/app/vendor/theme-init.js
|
||||
|
||||
ENV PYTHONPATH=/app \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
NEXUS_HOST=0.0.0.0 \
|
||||
NEXUS_PORT=8600
|
||||
|
||||
EXPOSE 8600
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||||
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
||||
# Nexus 6.0 鈥?production image (frontend build + API)
|
||||
# Build: docker compose -f docker/docker-compose.prod.yml build
|
||||
# See: docs/design/plans/2026-06-04-1panel-docker-production.md
|
||||
|
||||
# 鈹€鈹€ Stage 1: Vue/Vite SPA 鈹€鈹€
|
||||
FROM node:20-bookworm-slim AS frontend
|
||||
WORKDIR /build/frontend
|
||||
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
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 if [ -f package-lock.json ]; then npm ci --ignore-scripts; else npm install --ignore-scripts; fi
|
||||
|
||||
COPY frontend-v2/ ./
|
||||
RUN npx vite build
|
||||
# 鈹€鈹€ 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 \
|
||||
openssh-client \
|
||||
rsync \
|
||||
sshpass \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server/ ./server/
|
||||
COPY web/agent/ ./web/agent/
|
||||
COPY --from=frontend /build/web/app ./web/app/
|
||||
COPY --from=frontend_v2 /build/web/app-v2 ./web/app-v2/
|
||||
# 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 \
|
||||
web/app/vendor/theme.css \
|
||||
web/app/vendor/alpinejs.min.js \
|
||||
./web/app/vendor/
|
||||
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/vendor/alpinejs.min.js \
|
||||
&& test -f /app/web/app/vendor/tailwindcss-browser.js \
|
||||
&& test -f /app/web/app/vendor/theme.css \
|
||||
&& test -f /app/web/app/vendor/theme-init.js
|
||||
|
||||
ENV PYTHONPATH=/app \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
NEXUS_HOST=0.0.0.0 \
|
||||
NEXUS_PORT=8600
|
||||
|
||||
EXPOSE 8600
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||||
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
||||
|
||||
|
||||
|
||||
@@ -1,70 +1,71 @@
|
||||
# 1Panel OpenResty — Nexus reverse proxy (paste into site custom config)
|
||||
# Domain: api.synaglobal.vip
|
||||
# Upstream: Nexus Docker on 127.0.0.1:8600 (docker-compose.prod.yml)
|
||||
#
|
||||
# SSL: configure in 1Panel UI (Let's Encrypt). This snippet assumes HTTPS server block.
|
||||
|
||||
upstream nexus_api {
|
||||
server 127.0.0.1:8600;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# 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;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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_connect_timeout 10s;
|
||||
}
|
||||
|
||||
location /ws/ {
|
||||
access_log /www/sites/api.synaglobal.vip/log/ws.log nexus_ws;
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
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 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /app/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
}
|
||||
|
||||
location /agent/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Fallback: install mode or misc routes served by FastAPI
|
||||
location / {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
}
|
||||
# 1Panel OpenResty — Nexus reverse proxy (paste into site custom config)
|
||||
# Domain: api.synaglobal.vip
|
||||
# Upstream: Nexus Docker on 127.0.0.1:8600 (docker-compose.prod.yml)
|
||||
#
|
||||
# SSL: configure in 1Panel UI (Let's Encrypt). This snippet assumes HTTPS server block.
|
||||
|
||||
upstream nexus_api {
|
||||
server 127.0.0.1:8600;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# 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 500m;
|
||||
client_body_timeout 600s;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_connect_timeout 10s;
|
||||
}
|
||||
|
||||
location /ws/ {
|
||||
access_log /www/sites/api.synaglobal.vip/log/ws.log nexus_ws;
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
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 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /app/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
}
|
||||
|
||||
location /agent/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Fallback: install mode or misc routes served by FastAPI
|
||||
location / {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -19,21 +19,33 @@ echo "═══ Nexus Frontend Deploy ═══"
|
||||
|
||||
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" ]] && docker compose version >/dev/null 2>&1; then
|
||||
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 network inspect 1panel-network >/dev/null 2>&1 && \
|
||||
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" && docker compose "${args[@]}" \
|
||||
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
|
||||
|
||||
+76
-19
@@ -1,16 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Full production deploy: push (optional) + backend upgrade + frontend + health
|
||||
# Full production deploy: local rsync + backend upgrade + frontend + health
|
||||
#
|
||||
# 自动识别远程 Docker(/opt/nexus)或 Supervisor(/www/wwwroot/...)运行时。
|
||||
# 默认从本机工作区 rsync 到生产机,不 push Gitea、不远程 git pull。
|
||||
#
|
||||
# Prerequisites on YOUR machine:
|
||||
# - SSH: ~/.ssh/config Host nexus OR export NEXUS_SSH="azureuser@20.24.218.235"
|
||||
# - rsync, SSH Host nexus OR export NEXUS_SSH="azureuser@20.24.218.235"
|
||||
# - Key: export NEXUS_SSH_KEY=~/.ssh/id_rsa_nexus
|
||||
#
|
||||
# Usage:
|
||||
# cd "$NEXUS_ROOT"
|
||||
# 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
|
||||
|
||||
@@ -31,8 +34,15 @@ fi
|
||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||
scp_cmd() { scp "${SSH_OPTS[@]}" "$@"; }
|
||||
|
||||
DEPLOY_VIA_GIT="${NEXUS_DEPLOY_VIA_GIT:-0}"
|
||||
case "${DEPLOY_VIA_GIT}" in
|
||||
1|true|yes|on) DEPLOY_VIA_GIT=1 ;;
|
||||
*) DEPLOY_VIA_GIT=0 ;;
|
||||
esac
|
||||
|
||||
echo "═══ Nexus production deploy ═══"
|
||||
echo "SSH target: ${NEXUS_SSH_HOST}"
|
||||
echo "Mode: $([[ "$DEPLOY_VIA_GIT" == 1 ]] && echo 'Gitea git pull' || echo 'local rsync')"
|
||||
|
||||
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
||||
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
||||
@@ -94,27 +104,72 @@ fi
|
||||
echo "Deploy path: ${DEPLOY_PATH}"
|
||||
echo "Runtime: ${RUNTIME}"
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Docker: git pull + 镜像重建(前端在镜像内 vite build)..."
|
||||
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
|
||||
echo "▶ Supervisor: git pull + restart..."
|
||||
ssh_cmd "cd ${DEPLOY_PATH} && git fetch --all && git reset --hard origin/main && supervisorctl restart nexus"
|
||||
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}"
|
||||
|
||||
echo "▶ Frontend: build + upload..."
|
||||
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..."
|
||||
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"
|
||||
ssh_cmd "cd ${DEPLOY_PATH}/web/app && sudo tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz && sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app"
|
||||
|
||||
echo "▶ Frontend V2: local vite build:safe + upload to host web/app-v2..."
|
||||
cd frontend-v2 && npm run build:safe && cd ..
|
||||
tar czf /tmp/nexus-frontend-v2.tar.gz -C web/app-v2 index.html assets/
|
||||
scp_cmd /tmp/nexus-frontend-v2.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend-v2.tar.gz"
|
||||
ssh_cmd "sudo mkdir -p ${DEPLOY_PATH}/web/app-v2 && cd ${DEPLOY_PATH}/web/app-v2 && sudo tar xzf /tmp/nexus-frontend-v2.tar.gz && rm /tmp/nexus-frontend-v2.tar.gz && sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app-v2"
|
||||
|
||||
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
|
||||
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py --dry-run"
|
||||
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"
|
||||
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
|
||||
else
|
||||
echo "ERROR: 无法识别远程运行时(非 Docker 也非 Supervisor)" >&2
|
||||
echo " 请手动: ssh ${NEXUS_SSH_HOST} 'sudo nx update'" >&2
|
||||
exit 1
|
||||
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 sync failed — run sync_webapp_to_container.sh on server manually" >&2
|
||||
}
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
@@ -122,9 +177,11 @@ PORT="$(ssh_cmd "grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod
|
||||
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"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+145
-16
@@ -9,12 +9,64 @@ set -euo pipefail
|
||||
|
||||
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-/opt/nexus}"
|
||||
NEXUS_SERVICE="${NEXUS_SERVICE:-nexus}"
|
||||
FAIL_FILE="/tmp/nexus_health_fail_count"
|
||||
LAST_RESTART_FILE="/tmp/nexus_health_last_restart"
|
||||
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
|
||||
RESTART_COOLDOWN_SEC="${NEXUS_HEALTH_RESTART_COOLDOWN:-300}"
|
||||
ENV_PROD="${DEPLOY_DIR}/docker/.env.prod"
|
||||
|
||||
ensure_state_paths() {
|
||||
if [[ -n "$STATE_DIR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local preferred="${DEPLOY_DIR}/var/layer3-health"
|
||||
local fallback="${HOME:-/tmp}/.nexus/layer3-health"
|
||||
if mkdir -p "$preferred" 2>/dev/null && [[ -w "$preferred" ]]; then
|
||||
STATE_DIR="$preferred"
|
||||
elif command -v sudo >/dev/null 2>&1 && sudo mkdir -p "$preferred" 2>/dev/null \
|
||||
&& sudo chown -R "$(id -un):$(id -gn)" "$preferred" 2>/dev/null \
|
||||
&& [[ -w "$preferred" ]]; then
|
||||
STATE_DIR="$preferred"
|
||||
else
|
||||
mkdir -p "$fallback"
|
||||
STATE_DIR="$fallback"
|
||||
fi
|
||||
FAIL_FILE="${STATE_DIR}/fail_count"
|
||||
LAST_RESTART_FILE="${STATE_DIR}/last_restart"
|
||||
}
|
||||
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo docker "$@" 2>/dev/null
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
init_layer3_state() {
|
||||
ensure_state_paths
|
||||
chmod 755 "$STATE_DIR" 2>/dev/null || true
|
||||
if [[ ! -f "$FAIL_FILE" && -f "$LEGACY_FAIL_FILE" ]]; then
|
||||
cp "$LEGACY_FAIL_FILE" "$FAIL_FILE" 2>/dev/null || true
|
||||
fi
|
||||
if [[ ! -f "$LAST_RESTART_FILE" && -f "$LEGACY_LAST_RESTART_FILE" ]]; then
|
||||
cp "$LEGACY_LAST_RESTART_FILE" "$LAST_RESTART_FILE" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
write_state_file() {
|
||||
local path="$1" value="$2"
|
||||
init_layer3_state
|
||||
printf '%s\n' "$value" >"$path" 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
resolve_publish_port() {
|
||||
local port
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
@@ -25,7 +77,7 @@ PUBLISH_PORT="$(resolve_publish_port)"
|
||||
HEALTH_URL="http://127.0.0.1:${PUBLISH_PORT}/health"
|
||||
|
||||
resolve_nexus_container() {
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||||
docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||||
}
|
||||
|
||||
read_env_var() {
|
||||
@@ -50,7 +102,7 @@ load_telegram_from_db() {
|
||||
[[ -n "$cname" ]] || return 1
|
||||
[[ -n "${BOT_TOKEN:-}" && -n "${CHAT_ID:-}" ]] && return 0
|
||||
local out token chat
|
||||
out="$(docker exec "$cname" python3 -c "
|
||||
out="$(docker_cmd exec "$cname" python3 -c "
|
||||
import asyncio
|
||||
from server.config import settings as s
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
@@ -94,7 +146,7 @@ load_telegram_config() {
|
||||
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 exec "$cname" grep -E '^NEXUS_TELEGRAM_' /app/.env 2>/dev/null || true)
|
||||
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
|
||||
@@ -103,7 +155,7 @@ load_telegram_config() {
|
||||
}
|
||||
|
||||
send_telegram() {
|
||||
local msg="${1//\\n/$'\n'}" resp http log="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||
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
|
||||
@@ -121,24 +173,25 @@ send_telegram() {
|
||||
|
||||
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))
|
||||
echo "$fail" > "$FAIL_FILE"
|
||||
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))
|
||||
echo "$fail" > "$FAIL_FILE"
|
||||
write_state_file "$FAIL_FILE" "$fail" || true
|
||||
fi
|
||||
echo "$fail"
|
||||
}
|
||||
|
||||
reset_fail_count() {
|
||||
echo "0" > "$FAIL_FILE"
|
||||
write_state_file "$FAIL_FILE" "0" || true
|
||||
}
|
||||
|
||||
restart_cooldown_active() {
|
||||
@@ -151,13 +204,13 @@ restart_cooldown_active() {
|
||||
}
|
||||
|
||||
mark_restart_attempt() {
|
||||
date +%s > "$LAST_RESTART_FILE"
|
||||
write_state_file "$LAST_RESTART_FILE" "$(date +%s)" || true
|
||||
}
|
||||
|
||||
restart_nexus() {
|
||||
local cname="$1" via=""
|
||||
if [[ -n "$cname" ]]; then
|
||||
docker restart "$cname" >/dev/null 2>&1 && via="Docker 容器 ${cname}"
|
||||
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
|
||||
@@ -166,8 +219,84 @@ restart_nexus() {
|
||||
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)"
|
||||
|
||||
@@ -182,21 +311,21 @@ if [[ "$FAIL" -lt "$MAX_FAIL" ]]; then
|
||||
fi
|
||||
|
||||
if restart_cooldown_active; then
|
||||
send_telegram "🔴 <b>【Layer 3 宿主机巡检】健康检查连续失败(冷却期内)</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n连续失败:已达 ${MAX_FAIL} 次\nDocker 容器:${NEXUS_CONTAINER:-未识别}\n具体情况:/health 仍不可达,但距上次自动重启不足 ${RESTART_COOLDOWN_SEC} 秒,本次跳过重启以免频繁抖动\n建议操作:查看 docker logs ${NEXUS_CONTAINER:-nexus} 与 /var/log/nexus_health.log\n时间:${TS_LABEL}" || true
|
||||
telegram_health_fail_cooldown
|
||||
exit 0
|
||||
fi
|
||||
|
||||
send_telegram "🔴 <b>【Layer 3 宿主机巡检】Nexus 连续 ${MAX_FAIL} 次无响应</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n连续失败:${FAIL} 次(阈值 ${MAX_FAIL} 次)\nDocker 容器:${NEXUS_CONTAINER:-未识别}\n具体情况:宿主机 cron 无法访问 API 健康端点,平台可能已宕机或端口异常\n正在执行:自动重启 Nexus 服务…\n时间:${TS_LABEL}" || true
|
||||
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
|
||||
send_telegram "🟢 <b>【Layer 3 宿主机巡检】Nexus 已自动恢复</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n恢复方式:${VIA}\n具体情况:自动重启后 /health 检测通过,服务已恢复在线\n时间:${TS_LABEL}" || true
|
||||
telegram_health_recovered "$VIA"
|
||||
reset_fail_count
|
||||
else
|
||||
send_telegram "🔴 <b>【Layer 3 宿主机巡检】自动重启后仍未恢复</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n已尝试:${VIA}\n具体情况:重启完成后 /health 仍失败,可能存在数据库/Redis 故障或配置错误\n建议操作:立即 SSH 登录排查 docker logs、MySQL、Redis,需人工介入\n时间:${TS_LABEL}" || true
|
||||
telegram_health_still_down "$VIA"
|
||||
fi
|
||||
else
|
||||
send_telegram "🔴 <b>【Layer 3 宿主机巡检】无法执行自动重启</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n具体情况:未找到可重启的 Docker 容器或 Supervisor 服务(${NEXUS_SERVICE})\n建议操作:确认 nexus 容器名称与 docker ps 输出,手动执行 docker restart 或 nx update\n时间:${TS_LABEL}" || true
|
||||
telegram_health_restart_failed
|
||||
fi
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
+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 {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# 复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh
|
||||
# 用于 git push(scripts/git-push.sh);clone/pull 仍可匿名
|
||||
|
||||
# NEXUS_GITEA_HOST='66.154.115.8:3000'
|
||||
# NEXUS_GITEA_REPO='admin/Nexus.git'
|
||||
# admin 账号密码或 Access Token:
|
||||
# NEXUS_GITEA_TOKEN='你的密码或令牌'
|
||||
|
||||
# 新机安装勿填下面三项(install-nexus-fresh / --fresh 会自动按机生成)
|
||||
# NEXUS_SECRET_KEY=''
|
||||
# NEXUS_API_KEY=''
|
||||
# NEXUS_ENCRYPTION_KEY=''
|
||||
|
||||
NEXUS_DOMAIN='api.synaglobal.vip'
|
||||
NEXUS_ROOT='/opt/nexus'
|
||||
NEXUS_PROFILE='2c8g'
|
||||
NEXUS_FROM_ENV='/www/wwwroot/api.synaglobal.vip/.env'
|
||||
|
||||
# 本机生产 SSH(deploy-production.sh 读取;私钥路径勿提交真实 .pem)
|
||||
# NEXUS_SSH='azureuser@20.24.218.235'
|
||||
# NEXUS_SSH_KEY='/path/to/nz-admin.pem'
|
||||
Executable → Regular
+32
-5
@@ -50,6 +50,7 @@ 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}"
|
||||
@@ -181,9 +182,13 @@ 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"
|
||||
}
|
||||
@@ -1018,10 +1023,14 @@ verify_health() {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1138,8 +1147,12 @@ cmd_upgrade() {
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
resolve_profile
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
git_fetch
|
||||
show_pending || true
|
||||
if [[ "$SKIP_GIT" == true ]]; then
|
||||
info "跳过 Git 同步(本机 rsync 已推送代码)"
|
||||
else
|
||||
git_fetch
|
||||
show_pending || true
|
||||
fi
|
||||
|
||||
if [[ "$CHECK_ONLY" == true ]]; then
|
||||
exit 0
|
||||
@@ -1149,6 +1162,12 @@ cmd_upgrade() {
|
||||
exit 0
|
||||
fi
|
||||
|
||||
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
|
||||
@@ -1160,6 +1179,7 @@ cmd_upgrade() {
|
||||
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"
|
||||
@@ -1206,6 +1226,12 @@ cmd_upgrade() {
|
||||
docker image prune -f >/dev/null 2>&1 || true
|
||||
fi
|
||||
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
|
||||
@@ -1266,6 +1292,7 @@ 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 ;;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+64
-16
@@ -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}"
|
||||
@@ -130,12 +131,39 @@ else
|
||||
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"
|
||||
@@ -145,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
|
||||
@@ -172,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
|
||||
@@ -207,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
|
||||
@@ -234,7 +262,7 @@ 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)
|
||||
# 检查审计文件中是否列出了实际改动的文件
|
||||
@@ -273,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 "$@"
|
||||
Executable → Regular
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync local Nexus working tree to production host (no Gitea push/pull).
|
||||
# Transfer via tar|ssh; remote ownership auto-detected (www:www on BT, else deploy path owner).
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/rsync-local-to-server.sh
|
||||
# NEXUS_DEPLOY_PATH=/opt/nexus bash deploy/rsync-local-to-server.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT}"
|
||||
|
||||
SECRETS="${ROOT}/deploy/nexus-1panel.secrets.sh"
|
||||
if [[ -f "$SECRETS" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$SECRETS"
|
||||
fi
|
||||
|
||||
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
|
||||
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
|
||||
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
|
||||
fi
|
||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||
|
||||
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
||||
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-}"
|
||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
||||
DEPLOY_PATH="$(ssh_cmd 'for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do [[ -d "$d/server" ]] && echo "$d" && exit 0; done; echo /opt/nexus')"
|
||||
fi
|
||||
|
||||
resolve_deploy_chown() {
|
||||
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
|
||||
echo "${NEXUS_DEPLOY_CHOWN}"
|
||||
return
|
||||
fi
|
||||
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
|
||||
echo "www:www"
|
||||
return
|
||||
fi
|
||||
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
|
||||
}
|
||||
DEPLOY_CHOWN="$(resolve_deploy_chown)"
|
||||
|
||||
echo "═══ Nexus local → server sync ═══"
|
||||
echo "SSH target: ${NEXUS_SSH_HOST}"
|
||||
echo "Remote path: ${DEPLOY_PATH}"
|
||||
echo "Owner: ${DEPLOY_CHOWN}"
|
||||
|
||||
TAR_EXCLUDES=(
|
||||
--exclude='.git'
|
||||
--exclude='.env'
|
||||
--exclude='.env.*'
|
||||
--exclude='SECRETS.md'
|
||||
--exclude='deploy/nexus-1panel.secrets.sh'
|
||||
--exclude='docker/.env.prod'
|
||||
--exclude='web/data'
|
||||
--exclude='frontend/node_modules'
|
||||
--exclude='frontend-v2/node_modules'
|
||||
--exclude='node_modules'
|
||||
--exclude='.venv'
|
||||
--exclude='.venv-*'
|
||||
--exclude='venv'
|
||||
--exclude='__pycache__'
|
||||
--exclude='.pytest_cache'
|
||||
--exclude='.ruff_cache'
|
||||
--exclude='.cursor'
|
||||
--exclude='.playwright-mcp'
|
||||
--exclude='.install_locked'
|
||||
--exclude='.install_state.json'
|
||||
--exclude='web/app/assets'
|
||||
--exclude='tmp'
|
||||
--exclude='2025.2'
|
||||
--exclude='=2025.2'
|
||||
--exclude='*.bak-'
|
||||
--exclude='frontend/test-results'
|
||||
--exclude='frontend/e2e/.auth'
|
||||
)
|
||||
|
||||
echo "▶ tar stream → ${DEPLOY_PATH} ..."
|
||||
tar -C "${ROOT}" -czf - "${TAR_EXCLUDES[@]}" . | \
|
||||
ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "sudo tar -xzf - -C '${DEPLOY_PATH}'"
|
||||
|
||||
echo "▶ chown ${DEPLOY_CHOWN} (skip docker/.env.prod, web/data, .env) ..."
|
||||
ssh_cmd "sudo bash -s" <<REMOTE
|
||||
set -euo pipefail
|
||||
root='${DEPLOY_PATH}'
|
||||
own='${DEPLOY_CHOWN}'
|
||||
chown_tree() {
|
||||
local p="\$1"
|
||||
[[ -e "\$p" ]] || return 0
|
||||
sudo chown -R "\$own" "\$p"
|
||||
}
|
||||
for top in server deploy scripts tests standards docs frontend web; do
|
||||
chown_tree "\$root/\$top"
|
||||
done
|
||||
if [[ -d "\$root/docker" ]]; then
|
||||
while IFS= read -r -d '' p; do
|
||||
sudo chown -R "\$own" "\$p"
|
||||
done < <(find "\$root/docker" \( -path "\$root/docker/.env.prod" \) -prune -o -print0)
|
||||
fi
|
||||
for f in pyproject.toml requirements.txt requirements-dev.txt AGENTS.md CLAUDE.md; do
|
||||
[[ -f "\$root/\$f" ]] && sudo chown "\$own" "\$root/\$f"
|
||||
done
|
||||
REMOTE
|
||||
|
||||
echo "✓ sync complete (owner ${DEPLOY_CHOWN})"
|
||||
Executable → Regular
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync Git-tracked web/app and web/app-v2 into the running Nexus Docker container.
|
||||
#
|
||||
# Root cause: Dockerfile.prod runs vite build inside the image; Docker layer cache
|
||||
# can produce asset hashes that differ from committed web/app on the host.
|
||||
#
|
||||
# Usage (on production host after git pull / upgrade):
|
||||
# NEXUS_ROOT=/opt/nexus bash deploy/sync_webapp_to_container.sh
|
||||
#
|
||||
# Called automatically from deploy/nexus-1panel.sh cmd_upgrade and deploy-production.sh.
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
CONTAINER="${NEXUS_CONTAINER:-}"
|
||||
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
resolve_container() {
|
||||
if [[ -n "$CONTAINER" ]]; then
|
||||
return 0
|
||||
fi
|
||||
CONTAINER="$(docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$CONTAINER" ]]; then
|
||||
error "未找到运行中的 Nexus 容器(可设 NEXUS_CONTAINER=名称)"
|
||||
exit 1
|
||||
fi
|
||||
info "容器: $CONTAINER"
|
||||
}
|
||||
|
||||
verify_entry_bundle() {
|
||||
local html bundle_path code
|
||||
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/" 2>/dev/null || true)"
|
||||
bundle_path="$(printf '%s' "$html" | sed -n 's/.*src="\(\/app\/assets\/index-[^"]*\.js\)".*/\1/p' | head -1)"
|
||||
if [[ -z "$bundle_path" ]]; then
|
||||
error "/app/ index.html 未解析到主 bundle"
|
||||
exit 1
|
||||
fi
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${bundle_path}" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" != "200" ]]; then
|
||||
error "主 bundle ${bundle_path} → HTTP ${code}"
|
||||
exit 1
|
||||
fi
|
||||
info "入口校验 OK: ${bundle_path} → HTTP 200"
|
||||
}
|
||||
|
||||
verify_app_v2_entry_bundle() {
|
||||
local html bundle_path code
|
||||
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app-v2/" 2>/dev/null || true)"
|
||||
bundle_path="$(printf '%s' "$html" | sed -n 's/.*src="\(\/app-v2\/assets\/[^"]*\.js\)".*/\1/p' | head -1)"
|
||||
if [[ -z "$bundle_path" ]]; then
|
||||
error "/app-v2/ index.html 未解析到主 bundle"
|
||||
exit 1
|
||||
fi
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${bundle_path}" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" != "200" ]]; then
|
||||
error "V2 主 bundle ${bundle_path} → HTTP ${code}"
|
||||
exit 1
|
||||
fi
|
||||
info "V2 入口校验 OK: ${bundle_path} → HTTP 200"
|
||||
}
|
||||
|
||||
main() {
|
||||
local app="${NEXUS_ROOT}/web/app"
|
||||
local app_v2="${NEXUS_ROOT}/web/app-v2"
|
||||
if [[ ! -f "${app}/index.html" ]]; then
|
||||
error "缺少 ${app}/index.html,请先 git pull"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${app}/assets" ]]; then
|
||||
error "缺少 ${app}/assets"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "${app_v2}/index.html" ]]; then
|
||||
error "缺少 ${app_v2}/index.html,请先构建 frontend-v2"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${app_v2}/assets" ]]; then
|
||||
error "缺少 ${app_v2}/assets"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
resolve_container
|
||||
|
||||
info "同步 web/app → 容器 /app/web/app ..."
|
||||
docker_cmd exec "$CONTAINER" rm -rf /app/web/app/assets
|
||||
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app
|
||||
docker_cmd cp "${app}/index.html" "${CONTAINER}:/app/web/app/index.html"
|
||||
docker_cmd cp "${app}/assets" "${CONTAINER}:/app/web/app/assets"
|
||||
|
||||
if [[ -f "${app}/favicon.ico" ]]; then
|
||||
docker_cmd cp "${app}/favicon.ico" "${CONTAINER}:/app/web/app/favicon.ico" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
info "同步 web/app-v2 → 容器 /app/web/app-v2 ..."
|
||||
docker_cmd exec "$CONTAINER" rm -rf /app/web/app-v2/assets
|
||||
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app-v2
|
||||
docker_cmd cp "${app_v2}/index.html" "${CONTAINER}:/app/web/app-v2/index.html"
|
||||
docker_cmd cp "${app_v2}/assets" "${CONTAINER}:/app/web/app-v2/assets"
|
||||
|
||||
if [[ -x "${NEXUS_ROOT}/deploy/prune_frontend_assets.py" ]]; then
|
||||
info "容器内 prune 未引用 assets(可选)..."
|
||||
docker_cmd exec "$CONTAINER" python3 /app/deploy/prune_frontend_assets.py --dry-run 2>/dev/null || \
|
||||
warn "容器内 prune 跳过(脚本或路径不可用)"
|
||||
fi
|
||||
|
||||
verify_entry_bundle
|
||||
verify_app_v2_entry_bundle
|
||||
info "web/app 与 web/app-v2 同步完成"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+15
-8
@@ -1,8 +1,8 @@
|
||||
# Nexus 6.0 — Docker Compose (dev / staging)
|
||||
# Nexus 6.0 鈥?Docker Compose (dev / staging)
|
||||
# Usage:
|
||||
# cp docker/.env.example docker/.env # or: python docker/generate_env.py
|
||||
# docker compose up -d --build
|
||||
# open http://localhost:8600/app/
|
||||
# docker compose --env-file docker/.env up -d --build
|
||||
# open http://localhost:18600/app/
|
||||
|
||||
name: nexus
|
||||
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
retries: 8
|
||||
start_period: 40s
|
||||
ports:
|
||||
- "${MYSQL_PUBLISH_PORT:-3306}:3306"
|
||||
- "${MYSQL_PUBLISH_PORT:-13306}:3306"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
@@ -38,12 +38,18 @@ services:
|
||||
timeout: 3s
|
||||
retries: 8
|
||||
ports:
|
||||
- "${REDIS_PUBLISH_PORT:-6379}:6379"
|
||||
- "${REDIS_PUBLISH_PORT:-16379}:6379"
|
||||
|
||||
nexus:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
network: host
|
||||
args:
|
||||
DEBIAN_MIRROR: https://mirrors.aliyun.com/debian
|
||||
DEBIAN_SECURITY_MIRROR: https://mirrors.aliyun.com/debian-security
|
||||
PIP_INDEX_URL: https://mirrors.aliyun.com/pypi/simple/
|
||||
PIP_TRUSTED_HOST: mirrors.aliyun.com
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mysql:
|
||||
@@ -51,7 +57,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "${NEXUS_PUBLISH_PORT:-8600}:8600"
|
||||
- "${NEXUS_PUBLISH_PORT:-18600}:8600"
|
||||
environment:
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_PORT: "3306"
|
||||
@@ -64,11 +70,11 @@ services:
|
||||
NEXUS_DEPLOY_PATH: /app
|
||||
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD:-nexus_dev_pass}@mysql:3306/nexus
|
||||
NEXUS_REDIS_URL: redis://redis:6379/0
|
||||
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:-http://localhost:8600,http://127.0.0.1:8600}
|
||||
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:-http://localhost:18600,http://127.0.0.1:18600,http://localhost:8600,http://127.0.0.1:8600}
|
||||
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:-}
|
||||
NEXUS_API_KEY: ${NEXUS_API_KEY:-}
|
||||
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:-}
|
||||
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:-http://localhost:8600}
|
||||
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:-http://localhost:18600}
|
||||
env_file:
|
||||
- path: docker/.env
|
||||
required: false
|
||||
@@ -87,3 +93,4 @@ volumes:
|
||||
redis-data:
|
||||
nexus-state:
|
||||
nexus-web-data:
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Copy to docker/.env (or run: python docker/generate_env.py)
|
||||
# Used by docker compose for nexus service — never commit docker/.env
|
||||
|
||||
MYSQL_ROOT_PASSWORD=change_me_root
|
||||
MYSQL_PASSWORD=change_me_nexus
|
||||
|
||||
# Leave empty to use install wizard at http://localhost:8600/app/install.html
|
||||
# Or run generate_env.py to fill these three:
|
||||
NEXUS_SECRET_KEY=
|
||||
NEXUS_API_KEY=
|
||||
NEXUS_ENCRYPTION_KEY=
|
||||
|
||||
NEXUS_CORS_ORIGINS=http://localhost:8600,http://127.0.0.1:8600
|
||||
NEXUS_API_BASE_URL=http://localhost:8600
|
||||
|
||||
# Optional host port overrides
|
||||
# NEXUS_PUBLISH_PORT=8600
|
||||
# MYSQL_PUBLISH_PORT=3306
|
||||
@@ -1,33 +0,0 @@
|
||||
# Production Compose env — copy to docker/.env.prod (never commit)
|
||||
# Fresh install: bash deploy/install-nexus-fresh.sh (auto-generates NEXUS keys per host)
|
||||
# MySQL / Redis 不在 Docker 栈 — 请在宿主机或 1Panel 自建,安装向导步骤 3 填写
|
||||
|
||||
# Auto-generated on fresh install (unique per server). Do NOT copy between hosts.
|
||||
NEXUS_SECRET_KEY=
|
||||
NEXUS_API_KEY=
|
||||
NEXUS_ENCRYPTION_KEY=
|
||||
|
||||
# 1 = entrypoint must not write /app/.env until install wizard step 3
|
||||
NEXUS_INSTALL_WIZARD_PENDING=0
|
||||
|
||||
# Public URL
|
||||
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
|
||||
NEXUS_API_BASE_URL=https://api.synaglobal.vip
|
||||
|
||||
# Nexus container resource profile (docker/profiles/*.env)
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
|
||||
# 1Panel:nx update 自动探测并写入(安装向导步骤 3 预填)
|
||||
# NEXUS_1PANEL_DB_HOST=1Panel-mysql-xxxx
|
||||
# NEXUS_1PANEL_REDIS_HOST=1Panel-redis-xxxx
|
||||
|
||||
# 宿主机 Compose 插值/ nx update 同步用;安装向导完成后以卷内 /app/.env 为准(不经 Compose 注入)
|
||||
# 1Panel 有密码时: redis://:你的密码@1Panel-redis-xxxx:6379/0
|
||||
NEXUS_REDIS_URL=redis://host.docker.internal:6379/0
|
||||
|
||||
# 宿主机 git 仓库路径(容器内安装向导用于提示 nx cron / nx update)
|
||||
NEXUS_HOST_ROOT=/opt/nexus
|
||||
|
||||
# Optional
|
||||
# NEXUS_PUBLISH_PORT=8600
|
||||
+20
-20
@@ -1,20 +1,20 @@
|
||||
# MySQL 8.4 — Nexus production tuning for 1 vCPU / 4 GiB host (minimal)
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
innodb_buffer_pool_size = 512M
|
||||
innodb_log_file_size = 128M
|
||||
|
||||
max_connections = 80
|
||||
|
||||
innodb_read_io_threads = 2
|
||||
innodb_write_io_threads = 2
|
||||
|
||||
table_open_cache = 1000
|
||||
tmp_table_size = 32M
|
||||
max_heap_table_size = 32M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
# MySQL 8.4 — Nexus production tuning for 1 vCPU / 4 GiB host (minimal)
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
innodb_buffer_pool_size = 512M
|
||||
innodb_log_file_size = 128M
|
||||
|
||||
max_connections = 80
|
||||
|
||||
innodb_read_io_threads = 2
|
||||
innodb_write_io_threads = 2
|
||||
|
||||
table_open_cache = 1000
|
||||
tmp_table_size = 32M
|
||||
max_heap_table_size = 32M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
|
||||
+25
-25
@@ -1,25 +1,25 @@
|
||||
# MySQL 8.4 — Nexus production tuning for 2 vCPU / 8 GiB host
|
||||
# Mounted by docker/docker-compose.prod.yml (see docs/design/plans/2026-06-04-1panel-docker-production.md)
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
# ~1 GiB for InnoDB (leave RAM for Nexus, Redis, 1Panel, OS)
|
||||
innodb_buffer_pool_size = 1024M
|
||||
innodb_log_file_size = 256M
|
||||
|
||||
# Nexus async pool default 30+20; leave headroom for admin/migrations
|
||||
max_connections = 120
|
||||
|
||||
# 2 vCPU: avoid aggressive IO threads
|
||||
innodb_read_io_threads = 4
|
||||
innodb_write_io_threads = 4
|
||||
|
||||
# Sensible defaults on small VMs
|
||||
table_open_cache = 2000
|
||||
tmp_table_size = 64M
|
||||
max_heap_table_size = 64M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
# MySQL 8.4 — Nexus production tuning for 2 vCPU / 8 GiB host
|
||||
# Mounted by docker/docker-compose.prod.yml (see docs/design/plans/2026-06-04-1panel-docker-production.md)
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
# ~1 GiB for InnoDB (leave RAM for Nexus, Redis, 1Panel, OS)
|
||||
innodb_buffer_pool_size = 1024M
|
||||
innodb_log_file_size = 256M
|
||||
|
||||
# Nexus async pool default 30+20; leave headroom for admin/migrations
|
||||
max_connections = 120
|
||||
|
||||
# 2 vCPU: avoid aggressive IO threads
|
||||
innodb_read_io_threads = 4
|
||||
innodb_write_io_threads = 4
|
||||
|
||||
# Sensible defaults on small VMs
|
||||
table_open_cache = 2000
|
||||
tmp_table_size = 64M
|
||||
max_heap_table_size = 64M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
|
||||
+20
-20
@@ -1,20 +1,20 @@
|
||||
# MySQL 8.4 — Nexus production tuning for 4 vCPU / 16 GiB host
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
innodb_buffer_pool_size = 4096M
|
||||
innodb_log_file_size = 512M
|
||||
|
||||
max_connections = 200
|
||||
|
||||
innodb_read_io_threads = 8
|
||||
innodb_write_io_threads = 8
|
||||
|
||||
table_open_cache = 4000
|
||||
tmp_table_size = 128M
|
||||
max_heap_table_size = 128M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
# MySQL 8.4 — Nexus production tuning for 4 vCPU / 16 GiB host
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
innodb_buffer_pool_size = 4096M
|
||||
innodb_log_file_size = 512M
|
||||
|
||||
max_connections = 200
|
||||
|
||||
innodb_read_io_threads = 8
|
||||
innodb_write_io_threads = 8
|
||||
|
||||
table_open_cache = 4000
|
||||
tmp_table_size = 128M
|
||||
max_heap_table_size = 128M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Host profile: 1 vCPU / 4 GiB
|
||||
NEXUS_MEM_LIMIT=1g
|
||||
NEXUS_CPUS=1
|
||||
NEXUS_DB_POOL_SIZE=15
|
||||
NEXUS_DB_MAX_OVERFLOW=10
|
||||
# Host profile: 1 vCPU / 4 GiB
|
||||
NEXUS_MEM_LIMIT=1g
|
||||
NEXUS_CPUS=1
|
||||
NEXUS_DB_POOL_SIZE=15
|
||||
NEXUS_DB_MAX_OVERFLOW=10
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Host profile: 2 vCPU / 8 GiB — default production
|
||||
NEXUS_MEM_LIMIT=2g
|
||||
NEXUS_CPUS=1.5
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
# Host profile: 2 vCPU / 8 GiB — default production
|
||||
NEXUS_MEM_LIMIT=2g
|
||||
NEXUS_CPUS=1.5
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Host profile: 4 vCPU / 16 GiB
|
||||
NEXUS_MEM_LIMIT=4g
|
||||
NEXUS_CPUS=3
|
||||
NEXUS_DB_POOL_SIZE=50
|
||||
NEXUS_DB_MAX_OVERFLOW=30
|
||||
# Host profile: 4 vCPU / 16 GiB
|
||||
NEXUS_MEM_LIMIT=4g
|
||||
NEXUS_CPUS=3
|
||||
NEXUS_DB_POOL_SIZE=50
|
||||
NEXUS_DB_MAX_OVERFLOW=30
|
||||
|
||||
@@ -1,733 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Nexus 6.0 — 功能说明书</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0f172a;color:#e2e8f0;line-height:1.6}
|
||||
a{color:#60a5fa;text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
|
||||
/* Layout */
|
||||
.wrap{max-width:960px;margin:0 auto;padding:32px 24px 80px}
|
||||
|
||||
/* Header */
|
||||
.hero{text-align:center;padding:48px 0 40px;border-bottom:1px solid #1e293b;margin-bottom:40px}
|
||||
.hero h1{font-size:2.2rem;font-weight:700;color:#f8fafc;letter-spacing:-0.02em}
|
||||
.hero .sub{font-size:1rem;color:#64748b;margin-top:8px}
|
||||
.badge{display:inline-block;margin:4px;padding:3px 10px;border-radius:999px;font-size:0.72rem;font-weight:600}
|
||||
.badge-blue{background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb55}
|
||||
.badge-green{background:#14371f;color:#4ade80;border:1px solid #16a34a55}
|
||||
.badge-amber{background:#3d2a00;color:#fbbf24;border:1px solid #d9770655}
|
||||
.badge-red{background:#3b1212;color:#f87171;border:1px solid #dc262655}
|
||||
|
||||
/* TOC */
|
||||
.toc{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:20px 24px;margin-bottom:36px}
|
||||
.toc h2{font-size:0.85rem;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px}
|
||||
.toc ol{padding-left:18px;column-count:2;column-gap:24px}
|
||||
.toc li{margin:5px 0;font-size:0.88rem}
|
||||
|
||||
/* Section */
|
||||
.section{margin-bottom:48px}
|
||||
.section-title{font-size:1.4rem;font-weight:700;color:#f1f5f9;padding-bottom:10px;border-bottom:2px solid #334155;margin-bottom:24px;display:flex;align-items:center;gap:10px}
|
||||
.section-title .icon{font-size:1.2rem}
|
||||
|
||||
/* Card */
|
||||
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;margin-bottom:16px}
|
||||
.card-title{font-size:1.05rem;font-weight:600;color:#f1f5f9;margin-bottom:6px;display:flex;align-items:center}
|
||||
.card-title .num{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:#334155;font-size:0.7rem;font-weight:700;color:#94a3b8;margin-right:8px;shrink:0}
|
||||
.card-desc{color:#94a3b8;font-size:0.88rem;margin-bottom:14px}
|
||||
|
||||
/* Info rows */
|
||||
.info-grid{display:grid;grid-template-columns:1fr;gap:10px}
|
||||
.info-block{border-radius:8px;padding:12px 14px}
|
||||
.info-block.usage{background:#0f2d1f;border:1px solid #166534}
|
||||
.info-block.warn{background:#2a1a00;border:1px solid #92400e}
|
||||
.info-block.note{background:#1a1f2e;border:1px solid #4338ca}
|
||||
.info-title{font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
|
||||
.info-title.u{color:#4ade80}
|
||||
.info-title.w{color:#fbbf24}
|
||||
.info-title.n{color:#818cf8}
|
||||
.info-block ul{padding-left:16px;font-size:0.86rem;color:#cbd5e1}
|
||||
.info-block li{margin:4px 0}
|
||||
.info-block p{font-size:0.86rem;color:#cbd5e1}
|
||||
|
||||
/* Sub table */
|
||||
.feat-table{width:100%;border-collapse:collapse;font-size:0.86rem;margin-top:10px}
|
||||
.feat-table th{text-align:left;color:#64748b;font-weight:600;font-size:0.75rem;text-transform:uppercase;padding:6px 10px;border-bottom:1px solid #334155}
|
||||
.feat-table td{padding:7px 10px;border-bottom:1px solid #1e293b;color:#cbd5e1;vertical-align:top}
|
||||
.feat-table tr:last-child td{border-bottom:none}
|
||||
|
||||
/* Footer */
|
||||
footer{text-align:center;color:#334155;font-size:0.8rem;margin-top:60px;padding-top:24px;border-top:1px solid #1e293b}
|
||||
code{background:#334155;color:#7dd3fc;padding:2px 6px;border-radius:4px;font-size:0.83em;font-family:monospace}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<!-- HERO -->
|
||||
<div class="hero">
|
||||
<h1>⚡ Nexus 6.0</h1>
|
||||
<p class="sub">服务器运维管理平台 — 全站功能说明书 · 2026-05-23</p>
|
||||
<p class="sub" style="margin-top:10px;font-size:0.85rem">
|
||||
Markdown 维护版:<code>docs/project/nexus-full-site-features.md</code> ·
|
||||
AI 接续:<code>docs/project/AI-HANDOFF-2026-05-23.md</code>
|
||||
</p>
|
||||
<div style="margin-top:14px">
|
||||
<span class="badge badge-blue">FastAPI</span>
|
||||
<span class="badge badge-blue">SQLAlchemy 2.0</span>
|
||||
<span class="badge badge-blue">Redis 7</span>
|
||||
<span class="badge badge-blue">asyncssh</span>
|
||||
<span class="badge badge-blue">Tailwind v4</span>
|
||||
<span class="badge badge-green">2000+ 服务器</span>
|
||||
<span class="badge badge-green">JWT + TOTP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOC -->
|
||||
<div class="toc">
|
||||
<h2>目录</h2>
|
||||
<ol>
|
||||
<li><a href="#sitemap">全站页面地图</a></li>
|
||||
<li><a href="#network">网络与通道</a></li>
|
||||
<li><a href="#auth">认证与访问控制</a></li>
|
||||
<li><a href="#dashboard">仪表盘</a></li>
|
||||
<li><a href="#alerts">告警中心</a></li>
|
||||
<li><a href="#servers">服务器管理</a></li>
|
||||
<li><a href="#assets">资产管理</a></li>
|
||||
<li><a href="#webssh">Web SSH</a></li>
|
||||
<li><a href="#commands">命令与推送日志</a></li>
|
||||
<li><a href="#push">批量文件推送</a></li>
|
||||
<li><a href="#scripts">脚本与执行历史</a></li>
|
||||
<li><a href="#files">远程文件管理</a></li>
|
||||
<li><a href="#schedules">定时调度</a></li>
|
||||
<li><a href="#credentials">凭据管理</a></li>
|
||||
<li><a href="#retries">重试队列</a></li>
|
||||
<li><a href="#search">全局搜索</a></li>
|
||||
<li><a href="#settings">系统设置</a></li>
|
||||
<li><a href="#audit">审计日志</a></li>
|
||||
<li><a href="#install">安装向导</a></li>
|
||||
<li><a href="#guardian">3 层守护</a></li>
|
||||
<li><a href="#agent">子机 Agent</a></li>
|
||||
<li><a href="#background">后台任务</a></li>
|
||||
<li><a href="#acceptance">开发验收标准</a></li>
|
||||
<li><a href="#notimpl">未实现 / 已否决</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- 0. SITEMAP -->
|
||||
<div class="section" id="sitemap">
|
||||
<div class="section-title"><span class="icon">🗺</span> 全站页面地图(18 页)</div>
|
||||
<div class="card">
|
||||
<table class="feat-table">
|
||||
<tr><th>页面</th><th>路径</th><th>功能</th></tr>
|
||||
<tr><td>安装向导</td><td><code>/app/install.html</code></td><td>无 .env 时安装中心机</td></tr>
|
||||
<tr><td>登录</td><td><code>/app/login.html</code></td><td>JWT + TOTP + 防暴破</td></tr>
|
||||
<tr><td>仪表盘</td><td><code>/app/index.html</code></td><td>统计、WS 刷新统计</td></tr>
|
||||
<tr><td>服务器</td><td><code>/app/servers.html</code></td><td>CRUD、Agent 安装/升级、漂移徽章</td></tr>
|
||||
<tr><td>资产管理</td><td><code>/app/assets.html</code></td><td>Platform / Node</td></tr>
|
||||
<tr><td>文件管理</td><td><code>/app/files.html</code></td><td>SSH 浏览与文件操作</td></tr>
|
||||
<tr><td>推送</td><td><code>/app/push.html</code></td><td>rsync 批量 + dry-run</td></tr>
|
||||
<tr><td>脚本库</td><td><code>/app/scripts.html</code></td><td>脚本 CRUD、直接执行</td></tr>
|
||||
<tr><td>执行历史</td><td><code>/app/script-executions.html</code></td><td>停止/重试/日志</td></tr>
|
||||
<tr><td>凭据</td><td><code>/app/credentials.html</code></td><td>DB 凭据 + 密码预设</td></tr>
|
||||
<tr><td>定时调度</td><td><code>/app/schedules.html</code></td><td>Cron 推送或脚本</td></tr>
|
||||
<tr><td>重试队列</td><td><code>/app/retries.html</code></td><td>推送失败重试</td></tr>
|
||||
<tr><td>命令日志</td><td><code>/app/commands.html</code></td><td>WebSSH 命令 + 推送日志</td></tr>
|
||||
<tr><td>告警中心</td><td><code>/app/alerts.html</code></td><td>告警历史与统计</td></tr>
|
||||
<tr><td>审计</td><td><code>/app/audit.html</code></td><td>操作审计、多维过滤</td></tr>
|
||||
<tr><td>设置</td><td><code>/app/settings.html</code></td><td>阈值、Telegram、白名单、TOTP</td></tr>
|
||||
<tr><td>WebSSH</td><td><code>/app/terminal.html</code></td><td>xterm 终端</td></tr>
|
||||
</table>
|
||||
<p class="card-desc" style="margin-top:12px">共享:<code>layout.js</code>(侧栏/移动适配)、<code>api.js</code>(JWT、toast、全局搜索)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NETWORK -->
|
||||
<div class="section" id="network">
|
||||
<div class="section-title"><span class="icon">🔌</span> 网络与通道模型</div>
|
||||
<div class="card">
|
||||
<table class="feat-table">
|
||||
<tr><th>方向</th><th>端口</th><th>用途</th><th>必须</th></tr>
|
||||
<tr><td>子机 → 中心</td><td><strong>443 出站</strong></td><td>Agent 心跳、长任务 curl 回调</td><td>✅</td></tr>
|
||||
<tr><td>中心 → 子机</td><td><strong>22 入站</strong></td><td>推送、文件、脚本、健康检查、Agent 安装</td><td>✅</td></tr>
|
||||
<tr><td>子机本机</td><td><code>127.0.0.1:8601</code></td><td>Agent HTTP(可不暴露公网)</td><td>可选</td></tr>
|
||||
</table>
|
||||
<div class="info-block note" style="margin-top:12px"><div class="info-title n">说明</div><p>脚本执行与手动健康检查已改走 <strong>SSH</strong>,不依赖公网 8601。宝塔环境请用系统 Python 3.12 + <code>install.sh</code> venv,勿用面板 Python 项目管理器托管 Agent。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. AUTH -->
|
||||
<div class="section" id="auth">
|
||||
<div class="section-title"><span class="icon">🔒</span> 1. 认证与访问控制</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> JWT 登录 + 自动刷新</div>
|
||||
<p class="card-desc">管理员通过用户名+密码登录,获取 Access Token(30 分钟)和 Refresh Token(7 天)。前端到期前 2 分钟自动换新 Token,无感续期。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>访问 <code>/app/login.html</code>,输入管理员账号密码</li>
|
||||
<li>Token 存于 <code>localStorage</code>,关闭浏览器后 7 天内免登录</li>
|
||||
<li>8 小时无操作自动退出</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>JWT 存于 localStorage,XSS 漏洞可能导致 Token 泄露(已做 CSP 缓解)</li>
|
||||
<li>无 RBAC:只有一个管理员角色,鉴权即信任</li>
|
||||
<li>不支持多管理员角色区分</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> TOTP 双因素认证</div>
|
||||
<p class="card-desc">可选启用 Google Authenticator 兼容的 TOTP。登录时先验密码,再验 6 位动态码(30 秒窗口)。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>进入「设置」→「安全设置」→ 启用 / <strong>重新绑定</strong> TOTP,扫描二维码</li>
|
||||
<li>禁用 TOTP 需同时提供当前密码 + TOTP 码</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>TOTP 秘钥丢失无法恢复,需提前备份</li>
|
||||
<li>系统时间偏差 >30s 会导致验证失败(接受 ±1 窗口共 90s)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> 登录防暴破</div>
|
||||
<p class="card-desc">同一 IP + 用户名 15 分钟内失败 5 次,账号自动锁定并返回 HTTP 429。</p>
|
||||
<div class="info-block note"><div class="info-title n">注意</div><p>锁定基于 IP + 用户名组合,更换 IP 可绕过。适合防自动化爆破,不防分布式攻击。</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
|
||||
<p class="card-desc">可在设置中开启「仅允许白名单 IP 登录」。支持手动 IP/CIDR,以及从<strong>代理订阅 URL</strong>解析节点 IP(每 2 小时自动刷新,与手动列表合并)。</p>
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「设置」→「登录 IP 白名单」→ 总开关 + 订阅地址 + 手动 IP</li>
|
||||
<li>非白名单 IP 登录被拒绝并记审计</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. DASHBOARD -->
|
||||
<div class="section" id="dashboard">
|
||||
<div class="section-title"><span class="icon">📊</span> 2. 仪表盘与实时告警</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 实时统计面板</div>
|
||||
<p class="card-desc">首页展示服务器总数、在线数、离线数、告警中台数、最近推送记录。数据来源 Redis(实时)+ MySQL(历史),通过 WebSocket 触发刷新。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>在线状态取自 Redis <code>heartbeat:{id}</code>,子机 60s 上报一次</li>
|
||||
<li>告警计数 = Redis <code>alerts:*</code> key 数量</li>
|
||||
<li>WS 断线会自动重连(3 次指数退避)</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>在线状态最多滞后 60s(子机心跳间隔)</li>
|
||||
<li>Redis 不可用时统计退化为 MySQL 数据,可能不准</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 告警通知(Telegram)</div>
|
||||
<p class="card-desc">CPU / 内存 / 磁盘超过阈值(默认 80%)时,通过 Telegram Bot 推送告警;恢复后发送恢复通知。同服务器同指标 5 分钟内不重复推送。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「设置」配置 <code>TELEGRAM_BOT_TOKEN</code> + <code>TELEGRAM_CHAT_ID</code></li>
|
||||
<li>阈值可在设置页调整(CPU/内存/磁盘各独立)</li>
|
||||
<li>浏览器仪表盘<strong>只显示计数,不弹窗、不响铃</strong>(设计决策)</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>浏览器仪表盘不弹告警列表;历史见「告警中心」页</li>
|
||||
<li>Telegram Bot 被封或网络不通时告警静默失败(有日志)</li>
|
||||
<li>告警防抖 key 存于进程内存,重启后防抖状态重置</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ALERTS -->
|
||||
<div class="section" id="alerts">
|
||||
<div class="section-title"><span class="icon">🔔</span> 告警中心</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 告警历史(alerts.html)</div>
|
||||
<p class="card-desc">将 CPU/内存/磁盘告警与恢复事件持久化到 <code>alert_logs</code>,支持分页、按服务器/类型/日期筛选,以及近 7 日统计图表数据。</p>
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>侧栏「告警中心」查看历史列表</li>
|
||||
<li>API:<code>GET /api/alert-history/</code>、<code>GET /api/alert-history/stats</code></li>
|
||||
</ul></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 分项 Telegram 开关</div>
|
||||
<p class="card-desc">设置页可单独关闭某类告警的 Telegram 推送(告警/恢复/系统/时钟漂移等),便于后续扩展通道。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. SERVERS -->
|
||||
<div class="section" id="servers">
|
||||
<div class="section-title"><span class="icon">🖥</span> 3. 服务器管理</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 服务器 CRUD</div>
|
||||
<p class="card-desc">增删改查服务器配置,支持 SSH 密钥 / 密码双认证,可按平台类型(Platform)和节点(Node 树)分组。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>新增服务器:填写名称、域名/IP、SSH 端口、认证方式</li>
|
||||
<li>认证方式选「密钥」时粘贴私钥内容,自动 Fernet 加密存储</li>
|
||||
<li>支持 <code>platform_id</code> / <code>node_id</code> 分组归类</li>
|
||||
<li>服务器列表分页显示(默认 50 条/页)</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>批量导入(CSV)未实现,2000 台需逐一或通过 API 批量添加</li>
|
||||
<li>删除服务器同时删除关联推送日志(CASCADE)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 健康检查</div>
|
||||
<p class="card-desc">手动触发 <code>POST /api/servers/check</code>:经 <strong>SSH</strong> 执行 Python 采集 CPU/内存/磁盘(<code>remote_probe</code>),<strong>不需要</strong>公网 8601。</p>
|
||||
<div class="info-block note"><div class="info-title n">与在线状态区别</div><p>列表「在线」主要来自 Agent <strong>443 心跳</strong>;健康检查用于即时 SSH 探活与指标快照。</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">4</span> Agent 安装与升级</div>
|
||||
<p class="card-desc">服务器详情「Agent 安装」:复制 <code>install.sh</code> 命令或一键 SSH 安装(带日志);安装后轮询在线。升级通过 SSH 拉取最新 <code>agent.py</code> 并 restart。</p>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>安装脚本:Python 3.12 venv、监听 <code>127.0.0.1:8601</code>,无需公网放行 8601</li>
|
||||
<li>时钟漂移:心跳带 <code>agent_time</code>,列表显示 warn/crit 徽章</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> 每服务器 API Key</div>
|
||||
<p class="card-desc">为每台服务器单独生成 Agent API Key(格式 <code>nxs-xxx</code>),用于该服务器子机与中心通信的专属认证。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>服务器详情 → 「生成 Agent Key」,完整 Key 仅本次显示,立即复制到子机 <code>config.json</code></li>
|
||||
<li>列表页只显示前 8 位用于识别</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>Key 生成后无法再次查看(一次性展示设计)</li>
|
||||
<li>丢失需重新生成并更新子机配置</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ASSETS -->
|
||||
<div class="section" id="assets">
|
||||
<div class="section-title"><span class="icon">🗂</span> 资产管理</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> Platform 与 Node</div>
|
||||
<p class="card-desc"><code>assets.html</code> 管理资产类型模板(Platform)与树形节点(Node),服务器表单可关联分组。</p>
|
||||
<p class="card-desc">API:<code>/api/assets/platforms</code>、<code>/api/assets/nodes</code>(含 tree)</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> SSH 会话与命令(只读查询)</div>
|
||||
<p class="card-desc">同页或「命令日志」可查询 <code>ssh_sessions</code>、<code>command_logs</code>(WebSSH 产生)。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. WEB SSH -->
|
||||
<div class="section" id="webssh">
|
||||
<div class="section-title"><span class="icon">💻</span> 4. Web SSH 终端</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 浏览器终端</div>
|
||||
<p class="card-desc">基于 xterm.js + asyncssh,在浏览器内打开远程服务器 SSH 会话,支持全屏、自动 resize、彩色输出。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>服务器列表点击「终端」→ 弹出 <code>/app/terminal.html?server_id=X</code></li>
|
||||
<li>认证:先获取短效 webssh-token(15 分钟),绑定到该服务器,再建 WebSocket</li>
|
||||
<li>支持窗口 resize 自动通知远程 shell</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>Token 通过 URL 参数传递(?token=...),HTTPS 下安全;HTTP 下有风险</li>
|
||||
<li>不支持文件上传 / 拖拽(需用文件管理页)</li>
|
||||
<li>会话不录像(已明确否决)</li>
|
||||
<li>网络中断 WebSocket 不会自动重连,需刷新页面</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- COMMANDS -->
|
||||
<div class="section" id="commands">
|
||||
<div class="section-title"><span class="icon">💻</span> 命令与推送日志</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> WebSSH 会话与命令</div>
|
||||
<p class="card-desc"><code>commands.html</code>:会话列表、命令列表(危险命令高亮)、按服务器过滤。数据来自 WebSSH,直接 SSH 不记录。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 推送日志</div>
|
||||
<p class="card-desc">同页「推送日志」Tab:rsync 批量推送历史,支持服务器/状态/同步模式/触发方式过滤与分页。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. PUSH -->
|
||||
<div class="section" id="push">
|
||||
<div class="section-title"><span class="icon">📤</span> 5. 批量文件推送</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> rsync 批量推送</div>
|
||||
<p class="card-desc">将源目录通过 rsync-over-SSH 推送到选定的服务器列表,支持 4 种同步模式,内置分批(50 台/批)并发(10 路)控制。</p>
|
||||
<table class="feat-table">
|
||||
<tr><th>同步模式</th><th>rsync 参数</th><th>适用场景</th></tr>
|
||||
<tr><td>增量同步</td><td><code>-az</code></td><td>日常更新,只同步有变动的文件</td></tr>
|
||||
<tr><td>全量同步</td><td><code>-az --delete</code></td><td>强制目标与源完全一致,会删除目标多余文件</td></tr>
|
||||
<tr><td>校验和</td><td><code>-az --checksum</code></td><td>按文件内容而非时间戳判断差异</td></tr>
|
||||
<tr><td>直接覆盖</td><td><code>-az --inplace</code></td><td>不用临时文件,直接覆盖(大文件节省空间)</td></tr>
|
||||
</table>
|
||||
<div class="info-grid" style="margin-top:12px">
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li><strong>全量同步(--delete)会不可逆删除</strong>目标目录多余文件,务必先「预览」</li>
|
||||
<li>rsync 在中心机运行,中心机需能 SSH 到所有子机</li>
|
||||
<li>失败的服务器自动加入重试队列(指数退避,最多 100 次)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 推送前预览(dry-run)</div>
|
||||
<p class="card-desc">选中全量/覆盖模式时,页面显示橙色警告,引导先对首台服务器运行 <code>rsync --dry-run --stats</code>,查看待传输/删除文件数量后再决定是否推送。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>选择「全量同步」→ 出现橙色预览按钮 → 点击 → 展示首台服务器变动摘要</li>
|
||||
<li>可展开「详细文件列表」(最多 200 行)</li>
|
||||
<li>预览不阻塞推送,看完数据自行决定是否点「开始推送」</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>仅预览第一台选中的服务器,其余服务器可能略有差异</li>
|
||||
<li>预览需 SSH 连通,与真实推送消耗相同时间(通常 <5s)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> 失败自动重试队列</div>
|
||||
<p class="card-desc">推送失败的服务器自动进入 <code>push_retry_jobs</code> 队列,后台每 5 分钟按指数退避重试(60s → 120s → 240s...,最大 64 分钟)。</p>
|
||||
<div class="info-block note"><div class="info-title n">注意</div><p>最多重试 100 次。可在「重试」页手动强制立即重试或删除任务。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. SCRIPTS -->
|
||||
<div class="section" id="scripts">
|
||||
<div class="section-title"><span class="icon">📜</span> 脚本与执行历史</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 脚本库管理</div>
|
||||
<p class="card-desc">维护可复用 Shell 脚本库(按 ops/deploy/check/cleanup 分类),一键下发到选定的服务器。</p>
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「脚本」页 → 新建脚本 → 填写名称/类别/内容</li>
|
||||
<li>点击脚本名 → 编辑;点击「执行」→ 选择服务器 → 运行</li>
|
||||
<li>支持直接输入临时命令(无需保存到库)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 批量执行(短任务 / 长任务)</div>
|
||||
<p class="card-desc">短任务:同步等待结果(默认 30s 超时)。长任务:nohup 后台运行,完成后 curl 回调中心记录结果。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>选择服务器 → 填写命令 → 「执行」</li>
|
||||
<li>执行走 <strong>SSH 22</strong>(结果含 <code>channel: ssh</code>),无需子机公网 8601</li>
|
||||
<li>长任务:nohup + 子机 curl 回调中心(需 HTTPS <code>NEXUS_API_BASE_URL</code>)</li>
|
||||
<li>「执行历史」页:停止 / 重试 / 标记卡住 / 拉取 nexus-job 日志</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li><code>rm -rf /</code> 等危险命令被中心<strong>拦截(400)</strong>,但仅匹配预设规则</li>
|
||||
<li>长任务依赖子机能访问中心 <code>NEXUS_API_BASE_URL</code>(必须配置且使用 HTTPS)</li>
|
||||
<li>执行历史默认保留 2 小时 Redis 热数据,60s 刷入 MySQL</li>
|
||||
<li>执行命令时密码等敏感 $DB_* 变量存储时已脱敏(替换为 ***)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> $DB_* 数据库凭据替换</div>
|
||||
<p class="card-desc">脚本中使用 <code>$DB_USER</code> / <code>$DB_PASS</code> / <code>$DB_HOST</code> / <code>$DB_PORT</code> / <code>$DB_NAME</code> 占位符,执行时自动替换为选定的数据库凭据,密码不出现在脚本文本或日志中。</p>
|
||||
<div class="info-block note"><div class="info-title n">注意</div><p>替换在中心机完成后发往子机,子机接收到的已是明文命令;传输通过 HTTPS,子机侧无加密保护。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 7. FILES -->
|
||||
<div class="section" id="files">
|
||||
<div class="section-title"><span class="icon">📁</span> 7. 远程文件管理</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 目录浏览</div>
|
||||
<p class="card-desc">通过 SSH exec(<code>ls -la</code>)浏览指定服务器的远程目录,支持面包屑导航和目录点击进入。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「文件管理」→ 选服务器 → 输入路径 → 「浏览」(或按 Enter)</li>
|
||||
<li>点击目录名进入子目录,点击「..」返回上级</li>
|
||||
<li>文件按 目录优先 / 字母顺序 排列</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>无左侧树状面板(每次需手动导航)</li>
|
||||
<li>显示的文件大小为 <code>ls</code> 输出的字节数,大文件不友好</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 文件操作(删除/重命名/新建目录)</div>
|
||||
<p class="card-desc">鼠标悬停文件/目录行显示操作按钮,通过 SSH exec 在远程执行操作。</p>
|
||||
<table class="feat-table">
|
||||
<tr><th>操作</th><th>命令</th><th>触发方式</th></tr>
|
||||
<tr><td>删除文件</td><td><code>rm -f path</code></td><td>悬停 → 🗑 → confirm 弹窗</td></tr>
|
||||
<tr><td>删除目录</td><td><code>rm -rf path</code></td><td>悬停 → 🗑 → 额外警告弹窗</td></tr>
|
||||
<tr><td>重命名</td><td><code>mv src dst</code></td><td>悬停 → ✏ → prompt 输入新名</td></tr>
|
||||
<tr><td>新建目录</td><td><code>mkdir -p path</code></td><td>右上角 📁+ → prompt 输入名</td></tr>
|
||||
</table>
|
||||
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>删除目录(rm -rf)<strong>不可恢复</strong>,浏览器弹窗确认无法替代真正的安全保障</li>
|
||||
<li>无文件编辑器(已明确否决,用 WebSSH vim/nano 代替)</li>
|
||||
<li>无文件上传功能(可用 SFTP API 或 WebSSH scp)</li>
|
||||
<li>无文件内容预览</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 8. SCHEDULES -->
|
||||
<div class="section" id="schedules">
|
||||
<div class="section-title"><span class="icon">⏰</span> 8. 定时调度</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> Cron 调度(推送 + 脚本)</div>
|
||||
<p class="card-desc">支持两种 <code>schedule_type</code>:<strong>文件推送</strong> 或 <strong>脚本执行</strong>。5 字段 Cron,后台每 60s 检查(仅 Primary Worker)。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「定时」→ 选类型 → 填 Cron、目标服务器</li>
|
||||
<li>脚本型:选脚本库 ID 或内联内容,可设超时与长任务</li>
|
||||
<li>示例:<code>0 2 * * *</code> = 每天凌晨 2 点</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>Cron 精度受 60s 检查间隔限制</li>
|
||||
<li>脚本调度同样走 SSH 执行通道</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RETRIES -->
|
||||
<div class="section" id="retries">
|
||||
<div class="section-title"><span class="icon">🔄</span> 重试队列</div>
|
||||
<div class="card">
|
||||
<p class="card-desc"><code>retries.html</code> 管理推送失败任务:<code>retry_runner</code> 每 5 分钟指数退避;可手动立即重试或删除。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEARCH -->
|
||||
<div class="section" id="search">
|
||||
<div class="section-title"><span class="icon">🔍</span> 全局搜索</div>
|
||||
<div class="card">
|
||||
<p class="card-desc">侧栏搜索框调用 <code>GET /api/search/</code>,跨服务器、脚本、凭据、定时任务关键字匹配。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 9. CREDENTIALS -->
|
||||
<div class="section" id="credentials">
|
||||
<div class="section-title"><span class="icon">🔑</span> 9. 凭据管理</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 数据库凭据</div>
|
||||
<p class="card-desc">存储 MySQL/PostgreSQL 连接信息,密码 Fernet 加密,API 响应不含密码(仅元数据)。供脚本执行时通过 $DB_* 变量引用。</p>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><p>凭据只加密存储,子机接收到的命令已含明文连接信息(仅在中心→子机的 HTTPS 传输中受保护)。</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 密码预设</div>
|
||||
<p class="card-desc">存储 SSH 密码模板(Fernet 加密),可在推送配置中引用,避免重复输入常用密码。</p>
|
||||
<div class="info-block note"><div class="info-title n">注意</div><p>密码预设不能用于脚本 $DB_PASS 替换,只用于推送密码模板。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 10. SETTINGS -->
|
||||
<div class="section" id="settings">
|
||||
<div class="section-title"><span class="icon">⚙️</span> 10. 系统设置</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 可配置项</div>
|
||||
<table class="feat-table">
|
||||
<tr><th>设置项</th><th>说明</th><th>是否可改</th></tr>
|
||||
<tr><td>系统名称 / 标题</td><td>品牌展示名称</td><td>✅ 可改</td></tr>
|
||||
<tr><td>告警阈值(CPU/内存/磁盘)</td><td>超过阈值触发 Telegram 告警,默认 80%</td><td>✅ 可改</td></tr>
|
||||
<tr><td>Redis URL</td><td>Redis 连接地址</td><td>✅ 可改</td></tr>
|
||||
<tr><td>数据库连接池大小</td><td>pool_size / max_overflow</td><td>✅ 可改</td></tr>
|
||||
<tr><td>Telegram Bot Token / Chat ID</td><td>告警推送配置</td><td>✅ 可改</td></tr>
|
||||
<tr><td>SECRET_KEY / API_KEY / ENCRYPTION_KEY</td><td>核心安全密钥</td><td>❌ 不可改(需改 .env 重启)</td></tr>
|
||||
<tr><td>DATABASE_URL</td><td>数据库连接</td><td>❌ 不可改</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> Telegram</div>
|
||||
<p class="card-desc">配置 Bot Token / Chat ID;<strong>测试发送</strong>;通过 getUpdates <strong>检测 Chat ID</strong>;各告警类型独立开关。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> TOTP 与 API Key</div>
|
||||
<p class="card-desc">TOTP 启用/禁用/<strong>重新绑定</strong>;全局 API Key 需密码二次验证后查看(记审计)。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
|
||||
<p class="card-desc">总开关、订阅 URL(2h 刷新)、手动 IP、解析预览(见「认证」章节)。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 11. AUDIT -->
|
||||
<div class="section" id="audit">
|
||||
<div class="section-title"><span class="icon">📋</span> 11. 审计日志</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 操作审计</div>
|
||||
<p class="card-desc">所有 CUD 操作(创建/更新/删除服务器、推送、执行脚本、登录、改密码等)自动写入 <code>audit_logs</code>,记录操作人、IP、目标、详情。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「审计」页:日期起止、管理员用户名、操作类型(分组下拉)</li>
|
||||
<li>每页 50 条分页</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>无日志导出(已否决 CSV/JSON)</li>
|
||||
<li>审计日志不会自动清理,长期需归档</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 12. INSTALL -->
|
||||
<div class="section" id="install">
|
||||
<div class="section-title"><span class="icon">🚀</span> 12. 安装向导</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 5 步安装向导</div>
|
||||
<p class="card-desc">无 <code>.env</code> 文件时系统进入安装模式,访问 <code>/app/install.html</code> 完成 5 步配置:欢迎 → 数据库 → Redis → 管理员账号 → 完成。自动生成 API Key / Secret Key / Encryption Key。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>安装完成后 <code>.env</code> 自动生成,安装 API 除 GET /status 外全部锁定(返回 403)</li>
|
||||
<li>重装需手动删除 <code>.env</code> 并重启</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>安装过程未验证数据库/Redis 连接成功才允许进入下一步(应在步骤中手动验证)</li>
|
||||
<li>安装模式下除 install API 和静态文件外,其余 API 返回 503</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 13. GUARDIAN -->
|
||||
<div class="section" id="guardian">
|
||||
<div class="section-title"><span class="icon">🛡</span> 13. 三层守护机制</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 三层守护</div>
|
||||
<table class="feat-table">
|
||||
<tr><th>层级</th><th>机制</th><th>触发条件</th><th>动作</th></tr>
|
||||
<tr><td>Layer 1</td><td>Supervisor</td><td>Python 进程崩溃</td><td>自动重启</td></tr>
|
||||
<tr><td>Layer 2</td><td>Python self_monitor(30s)</td><td>Redis/MySQL 不可达</td><td>发 Telegram 告警/恢复</td></tr>
|
||||
<tr><td>Layer 3</td><td>Shell cron(1 分钟)</td><td>连续 3 次 /health 失败</td><td>supervisorctl restart + Telegram</td></tr>
|
||||
</table>
|
||||
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">注意</div><p>Shell cron 依赖宝塔/系统 crontab 配置,部署时需手动添加定时任务。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 14. AGENT -->
|
||||
<div class="section" id="agent">
|
||||
<div class="section-title"><span class="icon">🤖</span> 14. 子机 Agent</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> Agent 功能</div>
|
||||
<p class="card-desc">部署在每台被管服务器上的轻量服务:<strong>心跳走 443 出站</strong>;HTTP 仅本机 <code>127.0.0.1:8601</code>。中心侧脚本已改 <strong>SSH</strong>,/exec 端点保留供兼容。</p>
|
||||
<table class="feat-table">
|
||||
<tr><th>能力</th><th>说明</th></tr>
|
||||
<tr><td>心跳</td><td>POST 中心 <code>/api/agent/heartbeat</code>,写 Redis,智能加急</td></tr>
|
||||
<tr><td>时钟漂移</td><td><code>agent_time</code> 与中心比对,warn/crit + Telegram</td></tr>
|
||||
<tr><td>IP 白名单</td><td><code>allowed_ips</code> 限制访问本机 Agent HTTP</td></tr>
|
||||
<tr><td>长任务回调</td><td>POST <code>/api/agent/script-callback</code></td></tr>
|
||||
<tr><td>安装</td><td><code>web/agent/install.sh</code> — python3.12 venv + systemd</td></tr>
|
||||
</table>
|
||||
<div class="info-block warn" style="margin-top:12px"><div class="info-title w">注意</div><ul>
|
||||
<li>升级 API 存在,建议与 venv 安装脚本对齐后使用</li>
|
||||
<li>长任务回调需子机出站访问中心 HTTPS</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BACKGROUND -->
|
||||
<div class="section" id="background">
|
||||
<div class="section-title"><span class="icon">⚙</span> 后台任务(Primary Worker)</div>
|
||||
<div class="card">
|
||||
<table class="feat-table">
|
||||
<tr><th>任务</th><th>间隔</th><th>说明</th></tr>
|
||||
<tr><td>heartbeat_flush</td><td>10min</td><td>Redis 心跳 → MySQL</td></tr>
|
||||
<tr><td>self_monitor</td><td>30s</td><td>Redis/MySQL/WS 自检 + Telegram</td></tr>
|
||||
<tr><td>schedule_runner</td><td>60s</td><td>Cron 调度</td></tr>
|
||||
<tr><td>retry_runner</td><td>5min</td><td>推送重试</td></tr>
|
||||
<tr><td>ip_allowlist_refresh</td><td>2h</td><td>订阅 IP 刷新</td></tr>
|
||||
<tr><td>script_execution_flush</td><td>60s</td><td>执行记录 Redis→MySQL</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ACCEPTANCE -->
|
||||
<div class="section" id="acceptance">
|
||||
<div class="section-title"><span class="icon">✅</span> 标准与验收(与团队同等严谨)</div>
|
||||
<div class="card">
|
||||
<p class="card-desc"><strong>全量标准转接</strong>:<code>docs/project/standards-transfer-package.md</code> — 含 Cursor 规则、<code>standards/</code> 三件套、逐行八步 DoD、设计/上线清单。下一任 AI <strong>不得降低</strong>执行力度。</p>
|
||||
<table class="feat-table">
|
||||
<tr><th>类别</th><th>权威路径</th></tr>
|
||||
<tr><td>标准转接包</td><td><code>docs/project/standards-transfer-package.md</code></td></tr>
|
||||
<tr><td>验收 L0~L5</td><td><code>development-acceptance-standard.md</code></td></tr>
|
||||
<tr><td>系统开发</td><td><code>standards/system-development-standard.md</code></td></tr>
|
||||
<tr><td>逐行审计 v2</td><td><code>standards/line-walk-audit-standard-v2.md</code></td></tr>
|
||||
<tr><td>审计原则</td><td><code>standards/audit-core-principles.md</code></td></tr>
|
||||
<tr><td>Cursor</td><td><code>.cursor/rules/*.mdc</code>(含 audit-line-review)</td></tr>
|
||||
</table>
|
||||
<p class="card-desc" style="margin-top:12px">逐行审查:8 步/文件、Closure 全表含 SAFE、FINDING 必须 <code>文件:行号</code>、单回复 ≤5 文件。AI 接续:HANDOFF + 全站功能 + 标准转接包。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 15. NOT IMPL -->
|
||||
<div class="section" id="notimpl">
|
||||
<div class="section-title"><span class="icon">🚫</span> 15. 未实现 / 已否决功能</div>
|
||||
|
||||
<div class="card">
|
||||
<table class="feat-table">
|
||||
<tr><th>功能</th><th>状态</th><th>原因 / 替代方案</th></tr>
|
||||
<tr><td>文件编辑器(Monaco/ACE)</td><td><span class="badge badge-red">已否决</span></td><td>运维场景用 WebSSH + vim/nano;嵌入编辑器增加复杂度无实质收益</td></tr>
|
||||
<tr><td>会话录像(asciicast)</td><td><span class="badge badge-red">已否决</span></td><td>命令日志已满足审计需求</td></tr>
|
||||
<tr><td>RBAC 权限体系</td><td><span class="badge badge-red">已否决</span></td><td>鉴权即信任,单管理员模型;增加 RBAC 代价远超收益</td></tr>
|
||||
<tr><td>移动端适配</td><td><span class="badge badge-red">已否决</span></td><td>2000 台服务器运维不会用手机操作</td></tr>
|
||||
<tr><td>批量服务器导入(CSV)</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 API 批量 POST /api/servers/ 实现</td></tr>
|
||||
<tr><td>文件上传到远程服务器</td><td><span class="badge badge-amber">未实现</span></td><td>可用 WebSSH scp / 推送功能代替</td></tr>
|
||||
<tr><td>文件内容预览</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 WebSSH cat 查看</td></tr>
|
||||
<tr><td>WebSSH 断线自动重连</td><td><span class="badge badge-amber">未实现</span></td><td>当前需刷新页面;计划自动重试+按钮</td></tr>
|
||||
<tr><td>仪表盘历史趋势图</td><td><span class="badge badge-amber">未实现</span></td><td>仅实时 Redis 指标</td></tr>
|
||||
<tr><td>宝塔 API 监控集成</td><td><span class="badge badge-amber">未实现</span></td><td>可选;参考 btpanel-skills</td></tr>
|
||||
<tr><td>Agent mTLS</td><td><span class="badge badge-amber">讨论中</span></td><td>未实现</td></tr>
|
||||
<tr><td>首次部署 E2E(T1~T5、T-SE)</td><td><span class="badge badge-amber">待验证</span></td><td>见 production-verification-checklist.md</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<p>Nexus 6.0 全站功能说明书 · 2026-05-23(对齐代码库)</p>
|
||||
<p style="margin-top:6px">
|
||||
<a href="project/nexus-full-site-features.md">全站功能</a> ·
|
||||
<a href="project/standards-transfer-package.md">标准转接</a> ·
|
||||
<a href="project/development-acceptance-standard.md">验收标准</a> ·
|
||||
<a href="project/AI-HANDOFF-2026-05-23.md">AI 接续</a>
|
||||
</p>
|
||||
<p style="margin-top:4px">Python 3.12 / FastAPI 0.115 / MySQL 8.4 / Redis 7 / asyncssh 2.17 / Tailwind v4</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+14
-3
@@ -49,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. 设计与标准
|
||||
|
||||
| 目录 | 说明 |
|
||||
|------|------|
|
||||
@@ -60,13 +71,13 @@
|
||||
|
||||
---
|
||||
|
||||
## 5. 归档区
|
||||
## 6. 归档区
|
||||
|
||||
**[archive/README.md](archive/README.md)** — 2026-06 审计合并卷、旧 handoff、agentmemory、2026-05 审查报告。
|
||||
|
||||
---
|
||||
|
||||
## 6. 工具脚本
|
||||
## 7. 工具脚本
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# 审计 — 服务器页 Agent 诊断按钮
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-agent-diagnose-ui-button.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `agent_diagnose_service.py` | 中心 + SSH 诊断服务 |
|
||||
| `servers.py` | `POST /{id}/agent-diagnose` |
|
||||
| `agent_remote_diagnose.py` | CLI 改引用 service |
|
||||
| `AgentDiagnoseDialog.vue` | 诊断结果弹窗 |
|
||||
| `agentDiagnose.ts` | 前端类型 |
|
||||
| `ServersPage.vue` | 操作列按钮 |
|
||||
| `ServerUnsetPathPanel.vue` | 未设路径表操作列 |
|
||||
| `auditLabels.ts` | 审计 action 文案 |
|
||||
| `test_agent_diagnose_service.py` | 服务单测 |
|
||||
| `test_agent_remote_diagnose.py` | CLI 辅助单测 |
|
||||
|
||||
## Step 3
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 安全 | PASS — JWT + 审计;config 脱敏;无密钥回传 |
|
||||
| 行为 | PASS — 只读诊断 |
|
||||
|
||||
## Closure
|
||||
|
||||
- `pytest tests/test_agent_diagnose_service.py -q` PASS
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + audit
|
||||
- [x] 生产部署验证(`c5e54da`,/health ok,/app 200)
|
||||
@@ -0,0 +1,27 @@
|
||||
# 审计 — 子机 Agent 批量诊断脚本
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-agent-remote-diagnose-script.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `scripts/agent_remote_diagnose.py` | 中心侧批量 SSH 诊断 |
|
||||
| `scripts/agent_remote_diagnose.sh` | 包装入口 |
|
||||
| `tests/test_agent_remote_diagnose.py` | 解析/脱敏单测 |
|
||||
|
||||
## Step 3
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 安全 | PASS — 仅脱敏输出 api_key;SSH 凭据来自 DB 加密字段 |
|
||||
| 行为 | PASS — 只读诊断,无 CUD |
|
||||
|
||||
## Closure
|
||||
|
||||
- `pytest tests/test_agent_remote_diagnose.py -q` PASS
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + audit
|
||||
- [ ] 生产 `/opt/nexus` 同步脚本并验证 `--id 95`
|
||||
@@ -0,0 +1,42 @@
|
||||
# 审计 — 告警分页 + 调度单次 fire_at 修复(2026-06-09)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-alerts-schedule-fixes-deploy.md`
|
||||
|
||||
## 变更文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `AlertsPage.vue` | 每页条数状态 + `fetchPagePerPage` |
|
||||
| `settings.py` | 告警列表 `per_page` 上限 200 |
|
||||
| `schema_path_validators.py` | `coerce_optional_iso_datetime` |
|
||||
| `schemas.py` | `ScheduleCreate`/`ScheduleUpdate` `fire_at` → `datetime` |
|
||||
| `test_schedules.py` | once 推送创建集成测试 |
|
||||
| `prod_health_check.sh` | 生产公开/鉴权契约巡检 |
|
||||
| `index.html` | vite 构建 hash 更新 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 无静默吞错 | PASS — `loadAlerts` 失败 snackbar |
|
||||
| 调度 CUD 审计 | PASS — 未改 audit 写入逻辑 |
|
||||
| `fire_at` 类型安全 | PASS — schema 层解析,避免 ORM 500 |
|
||||
| 告警分页契约 | PASS — 与 `fetchPagePerPage` / 后端 `page`/`per_page` 一致 |
|
||||
| 无明文密钥 | PASS — 健康脚本用环境变量/SSH,不入仓 |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 结果 |
|
||||
|----|------|
|
||||
| `test_schedules.py` | 2 passed |
|
||||
| `test_alerts_audit.py` | 4 passed |
|
||||
| `vite build` | OK |
|
||||
| 生产复现 once 500 | 已确认根因;`3864d12` 部署后容器 healthy |
|
||||
| 部署验证报告 | `docs/reports/2026-06-09-alerts-schedule-fixes-deploy-verification.md` |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] 告警每页数目与 API `per_page` 联动
|
||||
- [x] 单次文件推送调度保存 201(非 500)
|
||||
- [x] Changelog + 本审计
|
||||
- [x] 生产部署与健康检查(`/health` ok,容器 healthy;鉴权 UI 待用户终验)
|
||||
@@ -0,0 +1,42 @@
|
||||
# 审计 — 接续 Wave0 + D-01(2026-06-09)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-continuation-wave0-d01.md`
|
||||
|
||||
## 变更文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `scripts/prod_probe.py` | 生产时区验收:422、cron next_run、bundle |
|
||||
| `deploy/sync_webapp_to_container.sh` | upgrade 后同步 Git web/app 进容器 |
|
||||
| `deploy/nexus-1panel.sh` | upgrade 末尾调用 sync |
|
||||
| `deploy/deploy-production.sh` | Docker 部署后 sync |
|
||||
| `deploy/deploy-frontend.sh` | sudo docker 运行时探测 |
|
||||
| `server/utils/agent_deploy.py` | D-01 统一 upgrade 命令 |
|
||||
| `server/api/servers.py` | 单台 upgrade 用 agent_deploy |
|
||||
| `server/application/services/server_batch_service.py` | 批量 upgrade + agent_port |
|
||||
| `tests/test_agent_deploy.py` | 单测 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 密钥 | PASS — 无凭据入仓 |
|
||||
| 静默吞错 | PASS — sync 失败 warn,probe 显式断言 |
|
||||
| Shell 注入 | PASS — docker cp 路径来自 NEXUS_ROOT |
|
||||
| 分层 | PASS — agent_deploy 在 utils,API 薄编排 |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 结果 |
|
||||
|----|------|
|
||||
| `test_agent_deploy.py` | passed |
|
||||
| `pre_deploy_check.sh` | 7/7 |
|
||||
| `prod_health_check.sh` origin | passed |
|
||||
| 生产 `sync_webapp_to_container.sh` | index-mgwiCK5j.js OK |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + audit + Handoff 2026-06-09
|
||||
- [x] 门控 7/7
|
||||
- [x] 生产探针全绿
|
||||
- [x] D-01 alignment-plan ☑
|
||||
@@ -0,0 +1,37 @@
|
||||
# 审计 — Dashboard 24h 舰队趋势(B-02)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-dashboard-24h-fleet-metrics.md`
|
||||
|
||||
## 变更文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server/utils/fleet_metrics.py` | 舰队均值聚合 |
|
||||
| `server/infrastructure/database/fleet_metric_repo.py` | 采样 CRUD + purge |
|
||||
| `server/background/heartbeat_flush.py` | 10min 写入样本 |
|
||||
| `server/api/servers.py` | `fleet-metrics` API |
|
||||
| `server/infrastructure/database/migrations.py` | `fleet_metric_samples` 表 |
|
||||
| `frontend/src/components/dashboard/DashboardFleetTrends.vue` | v-sparkline |
|
||||
| `frontend/src/pages/DashboardPage.vue` | 挂载趋势卡 |
|
||||
| `tests/test_fleet_metrics.py` | 单测 + API |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 分层 | PASS — API 只读,聚合在 utils/repo |
|
||||
| 性能 | PASS — 7 天约 1k 行;purge 每 flush |
|
||||
| 静默吞错 | PASS — 前端 load 失败显示空状态 |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 结果 |
|
||||
|----|------|
|
||||
| `test_fleet_metrics.py` | passed |
|
||||
| `npm run type-check` | passed |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] design + plan + changelog
|
||||
- [x] B-02 alignment-plan 待勾选
|
||||
- [x] 无新 npm 依赖
|
||||
@@ -0,0 +1,26 @@
|
||||
# 审计 — 移除舰队趋势 CSV 导出
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-fleet-trend-remove-csv.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `DashboardFleetTrends.vue` | 移除 CSV 按钮与 exportCsv |
|
||||
| `fleetTrendAnalytics.ts` | 删除 exportFleetTrendCsv |
|
||||
|
||||
## Step 3
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 安全 | PASS — 纯 UI 删除,无新入口 |
|
||||
| 行为 | PASS — 全屏明细表保留 |
|
||||
|
||||
## Closure
|
||||
|
||||
- `npm run type-check` PASS
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + audit
|
||||
- [x] 生产前端同步(`53b700f`,index-B00VieI3.js)
|
||||
@@ -0,0 +1,66 @@
|
||||
# 审计 — 全站测试体系交付(2026-06-09)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-full-site-testing-delivery.md`
|
||||
|
||||
## 变更文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `tests/integration/test_*.py` | IT 域集成测试(14 文件) |
|
||||
| `tests/chain/test_*.py` | 链路测试(8 文件,含 batch/exec 完成态) |
|
||||
| `tests/conftest.py` | 共享 fixture / SQLite 兼容 |
|
||||
| `tests/acceptance_catalog.py` | 验收目录扩展 |
|
||||
| `frontend/e2e/fixtures.mjs` | Worker 级 API 登录 + cookie 轮换写回 |
|
||||
| `frontend/e2e/helpers.mjs` | 会话恢复 + 侧栏 `nav` |
|
||||
| `frontend/e2e/pages/*.spec.mjs` | 页域 E2E(18) |
|
||||
| `frontend/e2e/full-acceptance.spec.mjs` | 15 步 serial 验收 |
|
||||
| `frontend/package.json` | `test:e2e` 脚本 |
|
||||
| `.gitea/workflows/ci-cd.yml` | CI 分层 pytest + E2E job |
|
||||
| `.gitignore` | 忽略 `frontend/e2e/.auth/`、`test-results/` |
|
||||
| `scripts/run_nexus_acceptance.py` | 验收脚本对齐 catalog |
|
||||
| `docs/testing/test-case-matrix.md` | 矩阵状态更新 |
|
||||
| `web/app/index.html` | vite build 入口 hash 更新 |
|
||||
| `frontend/src/api/index.ts` | 校验错误中文格式化接入 |
|
||||
| `frontend/src/utils/apiError.ts` | API 错误展示 |
|
||||
| `frontend/src/utils/validationErrorFormat.ts` | Pydantic detail → 中文 |
|
||||
| `frontend/src/pages/PushPage.vue` | 推送页异步提交 UX |
|
||||
| `frontend/src/pages/ServersPage.vue` | 服务器列表(无 CSV 导出) |
|
||||
| `frontend/src/components/push/PushToolbar.vue` | 推送工具栏 |
|
||||
| `frontend/src/composables/push/usePushProgress.ts` | 推送进度 composable |
|
||||
| `server/api/sync_v2.py` | sync 校验与异步推送 |
|
||||
| `server/background/sync_push_runner.py` | 后台推送 runner |
|
||||
| `server/utils/validation_errors_zh.py` | 后端校验中文映射 |
|
||||
| `tests/test_validation_errors_zh.py` | 中文校验单测 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 无静默吞错(测试 assert 真实行为) | PASS — 失败用例已修至 green |
|
||||
| 无明文密钥入仓 | PASS — E2E 密码仅环境变量 |
|
||||
| 分层:API 不测 UI、E2E 不替代 IT | PASS — 职责分离 |
|
||||
| E2E 并行不触发 login lockout | PASS — `fixtures.mjs` 每 worker 一次 API 登录 |
|
||||
| Refresh token 轮换 | PASS — 上下文结束写回 `storageState` |
|
||||
| SQLite/MySQL 测试兼容 | PASS — `SettingRepository` 等 monkeypatch/直连 |
|
||||
| CI 不阻塞于缺 E2E secret | PASS — E2E job 条件触发 |
|
||||
| 推送异步不阻塞 UI / 校验中文 | PASS — sync_v2 + PushPage + validation_errors_zh |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 结果 |
|
||||
|----|------|
|
||||
| pytest integration+chain | 27 passed |
|
||||
| pytest fast suite | 427 passed |
|
||||
| pytest slow (IT-INST) | 1 passed |
|
||||
| Playwright e2e | 35 passed |
|
||||
| local_integration_smoke | OK |
|
||||
| pre_deploy_check Gate 3–6 | PASS(本审计前) |
|
||||
| MCP-INST-001 各 step UI | ⏭ 用户确认不纳入验收 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] Wave 1–5 矩阵 IT/CH/E2E 缺口落地并有命令证据
|
||||
- [x] Playwright 35/35 本地 green(清理 assets 后复验)
|
||||
- [x] Changelog + 设计/计划/报告文档齐全
|
||||
- [x] 门控可过(今日 changelog + 本审计)
|
||||
- [ ] 生产部署(待用户批准 `deploy-production.sh`)
|
||||
@@ -0,0 +1,29 @@
|
||||
# 审计 — 离线统计准确性
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-offline-count-accuracy.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server_connectivity.py` | 逐台 Redis 计数 + status 筛选 |
|
||||
| `servers.py` | stats/list 对齐 |
|
||||
| `ServersPage.vue` | `offline_list` 展示 |
|
||||
| `test_server_connectivity.py` | 单测 |
|
||||
| `test_servers_dashboard.py` | stats 字段断言 |
|
||||
|
||||
## Step 3
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 安全 | PASS — 只读聚合 |
|
||||
| 行为 | PASS — 离线=Agent 监控且 Redis 非在线 |
|
||||
|
||||
## Closure
|
||||
|
||||
- `pytest tests/test_server_connectivity.py -q` PASS
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + audit
|
||||
- [x] 生产部署(含于 `7209f53`)
|
||||
@@ -0,0 +1,57 @@
|
||||
# 审计 — 运维时区全局北京(2026-06-09)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-operator-timezone-beijing.md` · `docs/changelog/2026-06-09-timezone-followups-ws-install-dates.md`
|
||||
**报告**: `docs/reports/2026-06-09-operator-timezone-beijing-audit.md`
|
||||
|
||||
## 变更文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server/utils/display_time.py` | `operator_wall`、`beijing_day_range_utc` |
|
||||
| `server/background/schedule_runner.py` | 北京墙钟 cron 匹配 |
|
||||
| `server/infrastructure/database/audit_log_repo.py` | 审计日期北京日界 |
|
||||
| `server/api/settings.py` | 告警 date 筛选、`utcnow` 修复 |
|
||||
| `frontend/src/utils/datetime.ts` | `beijingWallParts` |
|
||||
| `frontend/src/utils/scheduleCycle.ts` | 与 runner 对齐 |
|
||||
| `ScheduleCyclePicker.vue` | 北京时间提示 |
|
||||
| `tests/test_operator_tz.py` | 运维时区单测 |
|
||||
| `tests/test_schedule_next_run.py` | 更新北京语义断言 |
|
||||
| `server/api/websocket.py` | WS 告警/恢复消息增加 `at`(UTC ISO) |
|
||||
| `frontend/src/composables/useWebSocket.ts` | 使用 `msg.at` 展示北京时刻 |
|
||||
| `server/api/install.py` | 安装时区固定 `Asia/Shanghai` 校验 |
|
||||
| `web/app/install.html` | 移除误导性时区下拉,固定说明文案 |
|
||||
| `web/app/index.html` | 前端构建入口 hash 更新 |
|
||||
| `tests/test_ws_alert_at.py` | WS `at` 字段单测 |
|
||||
| `tests/integration/test_alerts_audit.py` | 非法日期 422 集成测 |
|
||||
| `frontend/e2e/pages/schedules-timezone.spec.mjs` | 调度北京时间 E2E |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| PY-11 naive/aware | PASS — `to_naive_utc` / `ensure_utc` |
|
||||
| 调度双触发 | PASS — 60s 冷却保留 |
|
||||
| SQL 注入 | PASS — 日期 `fromisoformat`,无拼接 |
|
||||
| 静默吞错 | PASS — 非法 `date_from`/`date_to` 返回 422 |
|
||||
| 存量 cron 语义 | RISK-P2 — 无迁移;部署后人工核对调度列表 |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 结果 |
|
||||
|----|------|
|
||||
| `test_operator_tz.py` | passed |
|
||||
| `test_schedule_next_run.py` | passed |
|
||||
| `test_ws_alert_at.py` | passed |
|
||||
| `test_alerts_audit.py` (422) | passed |
|
||||
| `scripts/local_verify.sh` | All local checks passed |
|
||||
| `npm run type-check` | passed |
|
||||
| ruff F | 0 violations |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] cron 按北京时间匹配,`next_run` 与表单时:分一致
|
||||
- [x] `cycleFromSchedule` 用 `parseApiDateTime` + 北京时:分
|
||||
- [x] 审计 `date_from` 覆盖北京当日 00:00–23:59
|
||||
- [x] design spec + plan + changelog
|
||||
- [x] WS 告警 `at`、安装时区固定、日期 422
|
||||
- [ ] 生产部署后人工核对启用中的调度(用户确认几乎无)
|
||||
@@ -0,0 +1,153 @@
|
||||
# 审计 — 子机离线告警 + 仪表盘统计/舰队趋势(`7209f53`)
|
||||
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-08-server-offline-alert.md`
|
||||
- `docs/changelog/2026-06-09-dashboard-stat-cards-align.md`
|
||||
- `docs/changelog/2026-06-09-fleet-trend-alerts.md`
|
||||
- `docs/changelog/2026-06-09-fleet-trend-interaction.md`
|
||||
- `docs/changelog/2026-06-09-offline-count-accuracy.md`
|
||||
|
||||
**部署验证**: `docs/reports/2026-06-09-server-offline-alert-deploy-verification.md`
|
||||
|
||||
## 审计信息
|
||||
|
||||
- **日期**: 2026-06-09
|
||||
- **审计人**: Cursor Agent
|
||||
- **触发原因**: 新功能(离线 Telegram 告警、设置开关)+ 仪表盘统计/趋势增强;已部署生产,补全门控审计
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 行数 | 状态 |
|
||||
|------|------|------|
|
||||
| `server/background/server_offline_monitor.py` | 88 | ☑ 已审 |
|
||||
| `server/api/websocket.py` | +37 | ☑ 已审 |
|
||||
| `server/infrastructure/telegram/__init__.py` | +37 | ☑ 已审 |
|
||||
| `server/utils/notify_toggles.py` | +5 | ☑ 已审 |
|
||||
| `server/config.py` | +2 | ☑ 已审 |
|
||||
| `server/api/settings.py` | +1 | ☑ 已审 |
|
||||
| `server/main.py` | +6 | ☑ 已审 |
|
||||
| `server/application/server_connectivity.py` | +7 | ☑ 已审 |
|
||||
| `server/utils/fleet_alert_buckets.py` | 88 | ☑ 已审 |
|
||||
| `server/api/servers.py` | +18 | ☑ 已审 |
|
||||
| `frontend/src/types/api.ts` | +1 | ☑ 已审 |
|
||||
| `frontend/src/composables/useServerStatsCards.ts` | 76 | ☑ 已审 |
|
||||
| `frontend/src/components/StatCardsRow.vue` | 136 | ☑ 已审 |
|
||||
| `frontend/src/pages/DashboardPage.vue` | Δ | ☑ 已审 |
|
||||
| `frontend/src/pages/ServersPage.vue` | Δ | ☑ 已审 |
|
||||
| `frontend/src/components/dashboard/DashboardFleetTrends.vue` | 363 | ☑ 已审 |
|
||||
| `frontend/src/components/dashboard/FleetTrendEChart.vue` | 新 | ☑ 已审 |
|
||||
| `frontend/src/components/dashboard/FleetTrendKpiStrip.vue` | 新 | ☑ 已审 |
|
||||
| `frontend/src/utils/fleetTrendAnalytics.ts` | 新 | ☑ 已审 |
|
||||
| `frontend/package.json` / `package-lock.json` | echarts 依赖 | ☑ 已审 |
|
||||
| `tests/test_server_offline_monitor.py` | 85 | ☑ 已审 |
|
||||
| `tests/test_fleet_alert_buckets.py` | 新 | ☑ 已审 |
|
||||
|
||||
## 审计8步结果
|
||||
|
||||
### Step 1: 登记 ✅
|
||||
|
||||
提交 `7209f53`,范围见上表;设计/计划文档齐全。
|
||||
|
||||
### Step 2: 全文 Read ✅
|
||||
|
||||
上述文件均已通读;无跨层直连(monitor → websocket → telegram;fleet-metrics → repo)。
|
||||
|
||||
### Step 3: 规则扫描 H
|
||||
|
||||
| ID | 规则 | 命中点 | 初判 |
|
||||
|----|------|--------|------|
|
||||
| H1 | 无明文密钥 | config/settings | SAFE |
|
||||
| H2 | 无静默吞错 | `server_offline_monitor` L86-87 `exc_info=True` | SAFE |
|
||||
| H3 | Telegram HTML 转义 | `send_telegram_offline_alert` `html.escape` name/hb | SAFE |
|
||||
| H4 | 通知开关门控 | `offline_alert_notify_enabled` + settings `MUTABLE_KEYS` | SAFE |
|
||||
| H5 | 边沿触发防轰炸 | `_prev_online` + `_should_push_telegram` 5min | SAFE |
|
||||
| H6 | 冷启动不误报 | `prev_online is None` 只记录 | SAFE |
|
||||
| H7 | 仅 primary worker | `main.py` 与 self_monitor 同锁 | SAFE |
|
||||
| H8 | API 鉴权 | `/fleet-metrics` `get_current_admin` | SAFE |
|
||||
| H9 | CUD 审计 | `notify_alert_offline` 走既有 PUT settings + audit | SAFE |
|
||||
| H10 | 离线判定一致性 | `heartbeat_indicates_online` 与 stats/list 共用 | SAFE |
|
||||
| H11 | 前端 XSS | 统计数字字符串化;ECharts 无 `v-html` 用户输入 | SAFE |
|
||||
| H12 | 依赖体积 | echarts ~625kB chunk | OBS(已知,懒加载页内) |
|
||||
|
||||
### Step 4: Closure表
|
||||
|
||||
| ID | 判定 | 依据 |
|
||||
|----|------|------|
|
||||
| H1 | SAFE | 无新增密钥字段 |
|
||||
| H2 | SAFE | tick 失败打 error 日志,不掩盖 |
|
||||
| H3 | SAFE | 与现有 `send_telegram_alert` 一致转义 |
|
||||
| H4 | SAFE | 关开关不推 Telegram;`alert_logs`/WS 仍记录(与资源告警策略一致) |
|
||||
| H5 | SAFE | 持续离线不重复;恢复后再离线可再推 |
|
||||
| H6 | SAFE | 单测 `test_cold_start_no_alert` |
|
||||
| H7 | SAFE | `_background_tasks` 仅 primary 注册 |
|
||||
| H8 | SAFE | 管理员 JWT |
|
||||
| H9 | SAFE | `MUTABLE_KEYS` 含 `notify_alert_offline` |
|
||||
| H10 | SAFE | `test_server_connectivity` |
|
||||
| H11 | SAFE | Vue 文本绑定 |
|
||||
| H12 | ACCEPT | 仪表盘页专用 chunk,可后续 code-split 优化 |
|
||||
|
||||
### Step 5: 入口表
|
||||
|
||||
| 入口 | 鉴权 | 说明 |
|
||||
|------|------|------|
|
||||
| 后台 `server_offline_monitor_loop` | primary worker | Redis 读 + DB 读 |
|
||||
| `broadcast_offline_alert` | 内部调用 | WS + alert_logs + Telegram |
|
||||
| `PUT /api/settings/notify_alert_offline` | admin JWT | 开关 |
|
||||
| `GET /api/servers/fleet-metrics` | admin JWT | 趋势+告警挂载 |
|
||||
| `GET /api/servers/stats` | admin JWT | 统计卡(既有) |
|
||||
| 设置页 `NOTIFY_TOGGLE_ITEMS` | 前端 | 展示开关 |
|
||||
|
||||
### Step 6: 输入 → Sink
|
||||
|
||||
| 路径 | Sink | 防护 |
|
||||
|------|------|------|
|
||||
| Redis `heartbeat:*` | 离线判定 | 只读;server_id 来自 DB 整数主键 |
|
||||
| `server_name` / `last_heartbeat` | Telegram HTML | `html.escape` |
|
||||
| `hours` query | fleet-metrics | `max(1,min(hours,168))` |
|
||||
| settings value | DB settings | 布尔字符串白名单流程 |
|
||||
|
||||
### Step 7: 归类
|
||||
|
||||
```
|
||||
server_offline_monitor.py 88行 12H 0FINDING
|
||||
websocket.py (offline块) 37行 12H 0FINDING
|
||||
telegram/__init__.py 37行 12H 0FINDING
|
||||
fleet_alert_buckets.py 88行 12H 0FINDING
|
||||
servers.py (fleet-metrics) 18行 12H 0FINDING
|
||||
frontend (统计+趋势) — 12H 0FINDING
|
||||
tests — 12H 0FINDING
|
||||
──────────────────────────────────────────────
|
||||
总计 12H 0FINDING
|
||||
```
|
||||
|
||||
### Step 8: DoD ✅
|
||||
|
||||
- [x] 设计文档 + 实施说明 + changelog
|
||||
- [x] 单测:`test_server_offline_monitor` 4、`test_fleet_alert_buckets`、`test_server_connectivity`、`test_fleet_metrics`
|
||||
- [x] `pytest` 相关 51 项通过(offline+fleet+notify)
|
||||
- [x] 门控 pre_deploy 7/7(部署前)
|
||||
- [x] 生产部署 `/health` ok、`/app/` 200(`7209f53`)
|
||||
- [ ] 浏览器终验:设置页「子机离线告警」开关 + 实机离线 Telegram(运维操作)
|
||||
|
||||
## FINDING 列表
|
||||
|
||||
无(0 FINDING)
|
||||
|
||||
## OBSERVATION(非阻断)
|
||||
|
||||
1. **离线发现延迟**:与 UI 一致,依赖 Redis `heartbeat:*` TTL(`FLUSH_INTERVAL×1.5` ≈ 15min)或键缺失;monitor 30s 轮询但状态变更是 TTL 驱动。设计文档已说明,与既有在线判定同源。
|
||||
2. **舰队趋势 tooltip**:`fleet_alert_buckets.ALERT_TYPE_LABELS` 未含 `offline`,趋势图钉显示原始 `offline` 而非「离线」——纯展示,可后续 1 行补齐。
|
||||
3. **`_prev_online` 内存**:删除子机后字典条目残留,量级可忽略;primary 重启会冷启动。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
pytest tests/test_server_offline_monitor.py tests/test_fleet_alert_buckets.py \
|
||||
tests/test_server_connectivity.py tests/test_fleet_metrics.py \
|
||||
tests/test_notify_toggle_sync.py -q
|
||||
# 51 passed
|
||||
```
|
||||
|
||||
## 结论
|
||||
|
||||
☑ **审计通过,0 FINDING**,已部署生产;待用户浏览器/实机离线 Telegram 终验。
|
||||
@@ -0,0 +1,32 @@
|
||||
# 审计 — 服务器页顶部统计卡交互
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-servers-stat-card-filter.md` · `docs/changelog/2026-06-09-docker-frontend-sync-fix.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `StatCardsRow.vue` | 可点击/active 态;5 卡布局 |
|
||||
| `useServerPagination.ts` | `is_online` 筛选 |
|
||||
| `ServersPage.vue` | 统计卡交互;未设路径定位 |
|
||||
| `ServerUnsetPathPanel.vue` | 高亮 id |
|
||||
| `servers.py` | stats `unset_path` |
|
||||
| `deploy-production.sh` | Docker 部署前本地 vite build |
|
||||
| `test_servers_dashboard.py` | unset_path 断言 |
|
||||
|
||||
## Step 3
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 安全 | PASS — 只读 stats + 既有 list 筛选 |
|
||||
| 行为 | PASS — 总数不可点;离线/在线/未设路径/告警可交互 |
|
||||
|
||||
## Closure
|
||||
|
||||
- `npm run type-check` PASS
|
||||
- `pytest tests/integration/test_servers_dashboard.py -q` PASS
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + audit
|
||||
- [ ] 生产部署验证
|
||||
@@ -0,0 +1,46 @@
|
||||
# 审计 — 服务层 ValueError 中文化 + HTTP detail 翻译层部署
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-service-valueerror-zh.md` · `docs/changelog/2026-06-09-http-api-detail-zh.md`
|
||||
|
||||
## 变更文件清单(本次 commit)
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server/api/schemas.py` | batch_id 校验文案中文化 |
|
||||
| `server/application/services/script_service.py` | 执行记录不存在 |
|
||||
| `server/application/services/server_batch_service.py` | 未知批量 op |
|
||||
| `server/infrastructure/database/crypto.py` | 凭据解密失败 |
|
||||
| `server/infrastructure/redis/script_execution_store.py` | Redis 执行记录不存在 |
|
||||
| `server/infrastructure/ssh/asyncssh_pool.py` | SSH 凭据缺失 |
|
||||
| `server/utils/schedule_cycle.py` | cron / 周期类型校验 |
|
||||
|
||||
## 一并部署(已在 main,非本 commit diff)
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server/utils/http_errors_zh.py` | 管理端 HTTP detail 翻译层(A) |
|
||||
| `server/main.py` | 全局 exception handler |
|
||||
| `server/api/auth.py` | auth_failure_detail |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 安全 | PASS — 仅改用户可见文案,无鉴权变更 |
|
||||
| 行为 | PASS — 异常类型与 HTTP 状态码不变 |
|
||||
| Agent API | PASS — `/api/agent/*` 仍跳过翻译 |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 结果 |
|
||||
|----|------|
|
||||
| `test_schedule_cycle.py` | passed |
|
||||
| `test_http_errors_zh.py` | passed |
|
||||
| `test_validation_errors_zh.py` | passed |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog
|
||||
- [x] 定向审计
|
||||
- [ ] 生产 deploy + 健康检查
|
||||
- [ ] 浏览器 spot-check 404 detail 中文
|
||||
@@ -0,0 +1,29 @@
|
||||
# 审计 — SSH 密钥预设列表修复
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-09-ssh-key-preset-list-fix.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `CredentialsManager.vue` | `http.getList` 替代 `fetchPagePerPage`;保存校验 |
|
||||
| `server/api/settings.py` | 列表 `private_key_set`;保存 PEM 校验 |
|
||||
| `server/utils/ssh_key_pem.py` | PEM 校验 + 公钥推导 |
|
||||
| `tests/test_ssh_key_pem.py` | RSA / OPENSSH 单测 |
|
||||
| `web/app/index.html` | Vite 构建产物入口 |
|
||||
|
||||
## Step 3
|
||||
|
||||
| 规则 | 结论 |
|
||||
|------|------|
|
||||
| 安全 | PASS — 私钥仍仅存服务端加密,列表不返回私钥 |
|
||||
| 行为 | PASS — 与密码预设 Tab 一致 |
|
||||
|
||||
## Closure
|
||||
|
||||
- `npm run type-check` PASS
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + audit
|
||||
- [ ] 生产前端热更新
|
||||
@@ -0,0 +1,35 @@
|
||||
# 审计 — 文件管理「新建文件」修复
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-10-files-new-file-fix.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `frontend/src/composables/files/useFilesActions.ts` | `openNewFileDialog`、等待编辑器就绪 |
|
||||
| `frontend/src/composables/files/useFilesEditor.ts` | 导出 `waitForEditorWorkbench` |
|
||||
| `frontend/src/composables/files/useFilesPage.ts` | 透传 helper |
|
||||
| `frontend/src/components/files/FilesToolbar.vue` | 未选服务器时提示 |
|
||||
| `server/infrastructure/ssh/asyncssh_pool.py` | SFTP 写前确保父目录 |
|
||||
|
||||
## Step 3(规则扫描)
|
||||
|
||||
| 规则 | 结论 | 说明 |
|
||||
|------|------|------|
|
||||
| 鉴权 | PASS | 仍走 `/sync/write-file` + admin JWT |
|
||||
| 路径 | PASS | `validatePathSegment` 不变 |
|
||||
| 静默失败 | PASS | 编辑器未就绪有 warning snackbar |
|
||||
| SSH | PASS | `mkdir -p` 仅父目录,不扩大写范围 |
|
||||
|
||||
## Closure
|
||||
|
||||
| 文件 | 结论 |
|
||||
|------|------|
|
||||
| `useFilesActions.ts` | SAFE — 无 v-html;错误经 snackbar |
|
||||
| `asyncssh_pool.py` | SAFE — 复用已有 `_ensure_remote_parent_dir` |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + 本审计
|
||||
- [x] frontend type-check
|
||||
- [ ] 生产:文件管理新建文件 → 编辑器打开
|
||||
@@ -0,0 +1,84 @@
|
||||
# 审计 — 浏览器 RDP(guacd)+ 3389远程页
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-10-rdp-browser-guacd.md`
|
||||
**设计**: `docs/design/specs/2026-06-10-rdp-browser-guacd-design.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `docker/docker-compose.prod.yml` | 增加 `guacd` 侧车、`NEXUS_GUACD_*` |
|
||||
| `docker-compose.yml` | 开发环境 guacd |
|
||||
| `server/config.py` | `GUACD_HOST` / `GUACD_PORT` |
|
||||
| `server/main.py` | 注册 `rdp_router` |
|
||||
| `server/api/rdp.py` | `WS /ws/rdp/{server_id}` JWT + guacd 桥 |
|
||||
| `server/api/servers.py` | `GET /{id}/rdp-connect`、RDP 字段 CRUD |
|
||||
| `server/api/schemas.py` | `rdp_*` 表单字段 |
|
||||
| `server/utils/rdp_config.py` | `extra_attrs` 存储、密码加密 |
|
||||
| `server/infrastructure/guacamole/tunnel.py` | WS↔TCP 透明转发 |
|
||||
| `frontend/package.json` | `guacamole-common-js` |
|
||||
| `frontend/src/App.vue` | 菜单「3389远程」 |
|
||||
| `frontend/src/router/index.ts` | `/rdp` 路由 |
|
||||
| `frontend/src/composables/useRoutePrefetch.ts` | 预载 RdpPage |
|
||||
| `frontend/src/composables/rdp/useRdpSession.ts` | Guacamole 客户端 |
|
||||
| `frontend/src/composables/rdp/useRdpServerList.ts` | RDP 服务器选择器 |
|
||||
| `frontend/src/pages/RdpPage.vue` | 独立页 + 选择器 |
|
||||
| `frontend/src/pages/ServersPage.vue` | 列表 RDP 跳转 |
|
||||
| `frontend/src/components/servers/ServerFormDialog.vue` | RDP 配置 UI |
|
||||
| `frontend/src/composables/servers/useServerFormDialog.ts` | RDP 表单 payload |
|
||||
| `frontend/src/types/api.ts` | `rdp_*` 类型 |
|
||||
| `frontend/src/types/guacamole.d.ts` | Guacamole 类型声明 |
|
||||
| `frontend/src/api/index.ts` | `downloadGet`(保留,非 RDP 主路径) |
|
||||
| `tests/test_rdp_config.py` | 配置与 connect 字符串单测 |
|
||||
|
||||
## 入口表
|
||||
|
||||
| 方法 | 路径 | 鉴权 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | `/api/servers/{id}/rdp-connect` | `get_current_admin` | 返回 hostname/port/user/**明文密码** |
|
||||
| WS | `/ws/rdp/{server_id}?token=` | access JWT(拒绝 `purpose=webssh`) | 校验 `rdp_enabled` 后桥接 guacd |
|
||||
| PUT/POST | `/api/servers` `rdp_*` 字段 | `get_current_admin` | 密码 `encrypt_value` 入 `extra_attrs` |
|
||||
|
||||
## Step 3(规则扫描)
|
||||
|
||||
| 规则 | 结论 | 说明 |
|
||||
|------|------|------|
|
||||
| 鉴权 | PASS | REST/WS 均需管理员 JWT;WS 拒绝 webssh 专用票 |
|
||||
| IDOR | PASS(平台模型) | 与终端/文件一致:任意管理员可连任意已启用 RDP 的服务器 |
|
||||
| 密钥落库 | PASS | `rdp_password` Fernet 加密存 `extra_attrs`;列表 API 仅 `rdp_password_set` |
|
||||
| 明文回传 | RISK 接受 | `rdp-connect` 向已登录管理员返回解密密码(Guacamole 客户端必需);HTTPS 依赖生产 TLS |
|
||||
| WS token 在 URL | RISK 已知 | 同 WebSSH/ADR-011;access JWT 非短票 |
|
||||
| 连接审计 | 设计外 | **故意无** `rdp_connect` / `SshSession` 审计(用户确认) |
|
||||
| guacd 暴露 | PASS | 仅 Docker 内网 `guacd:4822`,不映射宿主机端口 |
|
||||
| 静默吞错 | PASS | guacd 失败打 `warning`/`exception` 后 close WS;前端 `formatApiError` |
|
||||
| 注入 | PASS | 无 shell;Guacamole 参数由服务端校验后前端 encodeURIComponent |
|
||||
| 依赖 | RISK 低 | `guacamole-common-js@1.5.0` npm 包;`guacd` 官方镜像 |
|
||||
|
||||
## Closure(核心文件走读)
|
||||
|
||||
| 文件 | 行数 | 结论 |
|
||||
|------|------|------|
|
||||
| `server/api/rdp.py` | 110 | SAFE — JWT+tv 校验;`rdp_enabled` 后才 `accept`;无审计为设计项 |
|
||||
| `server/infrastructure/guacamole/tunnel.py` | 58 | SAFE — 纯字节转发;断开时 cancel task + close writer |
|
||||
| `server/utils/rdp_config.py` | 151 | SAFE — 密码 encrypt/decrypt;`apply_rdp_payload` 空密码可 clear |
|
||||
| `server/api/servers.py`(rdp-connect) | §1141 | SAFE — 503 无 guacd;400 无密码/未启用 |
|
||||
| `frontend/.../useRdpSession.ts` | 144 | SAFE — token 来自 auth store;密码仅内存+connect 字符串 |
|
||||
| `frontend/.../RdpPage.vue` | 185 | SAFE — 仅选 `rdp_enabled` 服务器;无 v-html |
|
||||
|
||||
**规则零命中(额外 P0/P1)**:无未授权 RCE、无 SQL 拼接、无硬编码生产密钥。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
pytest tests/test_rdp_config.py -q # 4 passed
|
||||
cd frontend && npm run type-check # pass
|
||||
cd frontend && npx vite build # pass(含 guacamole-common-js)
|
||||
```
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] 设计文档 + changelog
|
||||
- [x] Step 3 + Closure + 入口表
|
||||
- [x] 单测与前端 type-check
|
||||
- [ ] 生产部署:guacd 容器 + `NEXUS_GUACD_HOST` + 前端 sync(待执行)
|
||||
- [ ] 浏览器验收:侧栏「3389远程」→ 选机 → 画布连接(待生产)
|
||||
@@ -0,0 +1,66 @@
|
||||
# 审计 — 移除浏览器 RDP 功能
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-10-rdp-feature-removal.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server/api/rdp.py` | 删除 WS 隧道 |
|
||||
| `server/api/rdp_hosts.py` | 删除 CRUD |
|
||||
| `server/api/servers.py` | 删除 `/rdp-connect` 与 rdp 字段 |
|
||||
| `server/api/schemas.py` | 删除 rdp_* 请求字段 |
|
||||
| `server/main.py` | 注销 rdp 路由 |
|
||||
| `server/config.py` | 删除 GUACD_* |
|
||||
| `server/domain/models/__init__.py` | 删除 RdpHost |
|
||||
| `server/infrastructure/database/migrations.py` | DROP rdp_hosts |
|
||||
| `server/infrastructure/database/rdp_host_repo.py` | 删除 |
|
||||
| `server/infrastructure/guacamole/__init__.py` | 删除 |
|
||||
| `server/infrastructure/guacamole/protocol.py` | 删除 |
|
||||
| `server/infrastructure/guacamole/tunnel.py` | 删除 |
|
||||
| `server/infrastructure/guacamole/ws_tunnel.py` | 删除 |
|
||||
| `server/utils/rdp_config.py` | 删除 |
|
||||
| `docker-compose.yml` | 删除 guacd 服务 |
|
||||
| `docker/docker-compose.prod.yml` | 删除 guacd 侧车 |
|
||||
| `frontend/src/App.vue` | 移除侧栏 3389 |
|
||||
| `frontend/src/router/index.ts` | 移除 /rdp 路由 |
|
||||
| `frontend/src/composables/useRoutePrefetch.ts` | 移除预载 |
|
||||
| `frontend/src/pages/RdpPage.vue` | 删除 |
|
||||
| `frontend/src/components/rdp/RdpHostFormDialog.vue` | 删除 |
|
||||
| `frontend/src/composables/rdp/useRdpSession.ts` | 删除 |
|
||||
| `frontend/src/composables/rdp/useRdpHostList.ts` | 删除 |
|
||||
| `frontend/src/types/guacamole.d.ts` | 删除 |
|
||||
| `frontend/src/types/api.ts` | 删除 rdp 响应字段 |
|
||||
| `frontend/src/api/index.ts` | 注释更新 |
|
||||
| `frontend/package.json` | 移除 guacamole-common-js |
|
||||
| `frontend/package-lock.json` | 锁文件同步 |
|
||||
| `frontend/e2e/helpers.mjs` | 移除侧栏项 |
|
||||
| `tests/test_rdp_hosts.py` | 删除 |
|
||||
| `tests/test_rdp_config.py` | 删除 |
|
||||
| `tests/test_guacamole_protocol.py` | 删除 |
|
||||
|
||||
## Step 3(规则扫描)
|
||||
|
||||
| 规则 | 结论 | 说明 |
|
||||
|------|------|------|
|
||||
| 攻击面 | PASS | 移除 WS 隧道与 guacd 依赖,减少暴露 |
|
||||
| 密钥 | PASS | rdp_hosts 表 DROP;无新增凭据路径 |
|
||||
| 鉴权 | N/A | 删除端点,无新 API |
|
||||
| 静默吞错 | PASS | 无新增异常处理 |
|
||||
| 依赖 | PASS | 移除 guacamole-common-js |
|
||||
|
||||
## Closure(走读)
|
||||
|
||||
| 文件 | 结论 |
|
||||
|------|------|
|
||||
| `main.py` | SAFE — 无残留 rdp import |
|
||||
| `servers.py` | SAFE — 无 rdp_config 引用 |
|
||||
| `migrations.py` | SAFE — DROP IF EXISTS 幂等 |
|
||||
| `App.vue` | SAFE — 菜单项已移除 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + 本审计
|
||||
- [x] `local_verify` + frontend type-check
|
||||
- [ ] 生产部署与健康检查
|
||||
- [ ] 侧栏无「3389远程」
|
||||
@@ -0,0 +1,63 @@
|
||||
# 审计 — 3389 独立 RDP 主机(rdp_hosts)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-10-rdp-hosts-standalone.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server/domain/models/__init__.py` | `RdpHost` 模型 |
|
||||
| `server/infrastructure/database/migrations.py` | `rdp_hosts` 表 |
|
||||
| `server/infrastructure/database/rdp_host_repo.py` | 仓储 |
|
||||
| `server/api/rdp_hosts.py` | CRUD + connect REST |
|
||||
| `server/api/rdp.py` | WS `/ws/rdp/hosts/{host_id}` |
|
||||
| `server/main.py` | 注册 `rdp_hosts_router` |
|
||||
| `frontend/src/pages/RdpPage.vue` | 列表/添加/连接 |
|
||||
| `frontend/src/components/rdp/RdpHostFormDialog.vue` | 添加/编辑对话框 |
|
||||
| `frontend/src/composables/rdp/useRdpHostList.ts` | 列表与 CUD |
|
||||
| `frontend/src/composables/rdp/useRdpServerList.ts` | 删除(改由 useRdpHostList) |
|
||||
| `frontend/src/composables/rdp/useRdpSession.ts` | 改连 `rdp_hosts` |
|
||||
| `web/app/index.html` | Vite 构建入口 hash 更新 |
|
||||
| `frontend/src/components/servers/ServerFormDialog.vue` | 移除 RDP 区块 |
|
||||
| `frontend/src/composables/servers/useServerFormDialog.ts` | 移除 rdp 字段 |
|
||||
| `frontend/src/pages/ServersPage.vue` | 移除 RDP 跳转 |
|
||||
| `tests/test_rdp_hosts.py` | 模型/端口单测 |
|
||||
|
||||
## 入口表
|
||||
|
||||
| 方法 | 路径 | 鉴权 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | `/api/rdp/hosts/` | admin JWT | 列表(无密码) |
|
||||
| POST | `/api/rdp/hosts/` | admin JWT + 审计 | 创建,密码 Fernet |
|
||||
| PUT | `/api/rdp/hosts/{id}` | admin JWT + 审计 | 更新 |
|
||||
| DELETE | `/api/rdp/hosts/{id}` | admin JWT + 审计 | 删除 |
|
||||
| GET | `/api/rdp/hosts/{id}/connect` | admin JWT | 返回明文密码供 Guacamole |
|
||||
| WS | `/ws/rdp/hosts/{id}?token=` | access JWT | guacd 透明桥 |
|
||||
|
||||
## Step 3(规则扫描)
|
||||
|
||||
| 规则 | 结论 | 说明 |
|
||||
|------|------|------|
|
||||
| 鉴权 | PASS | 全部 REST/WS 需管理员 JWT |
|
||||
| IDOR | PASS(平台模型) | 与终端一致:管理员可连任意 RDP 主机 |
|
||||
| 密钥落库 | PASS | `password` 列 Fernet;列表仅 `password_set` |
|
||||
| 明文 connect | RISK 接受 | Guacamole 客户端必需;HTTPS |
|
||||
| CUD 审计 | PASS | create/update/delete_rdp_host |
|
||||
| guacd | PASS | 仍仅内网侧车 |
|
||||
| 静默吞错 | PASS | API HTTPException;WS close code |
|
||||
|
||||
## Closure(走读)
|
||||
|
||||
| 文件 | 结论 |
|
||||
|------|------|
|
||||
| `rdp_hosts.py` | SAFE — 名称唯一 IntegrityError→409;connect 校验 guacd |
|
||||
| `rdp_host_repo.py` | SAFE — 标准 CRUD |
|
||||
| `rdp.py` | SAFE — host 存在性校验后桥接 |
|
||||
| `RdpPage.vue` | SAFE — 无 v-html;添加后 `host_id` 连接 |
|
||||
| `useRdpHostList.ts` | SAFE — formatApiError |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + 本审计
|
||||
- [x] `local_verify` + type-check
|
||||
- [ ] 生产部署与浏览器验收
|
||||
@@ -0,0 +1,36 @@
|
||||
# 审计 — 内置 Web 浏览器
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/pages/BrowserPage.vue` | iframe 浏览器页 |
|
||||
| `frontend/src/composables/useEmbeddedBrowser.ts` | 导航历史 |
|
||||
| `frontend/src/utils/browserUrl.ts` | URL 白名单校验 |
|
||||
| `frontend/src/router/index.ts` | `/browser` 路由 |
|
||||
| `frontend/src/App.vue` | 侧栏 + 全屏布局 |
|
||||
| `frontend/src/pages/ServersPage.vue` | 站点入口 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 无 SSRF(无后端 fetch) | PASS |
|
||||
| H2 | 仅 http/https | PASS — normalizeBrowserUrl |
|
||||
| H3 | iframe sandbox | PASS |
|
||||
| H4 | 鉴权 | PASS — 走路由 guard |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | 纯前端 iframe |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] type-check
|
||||
- [x] changelog
|
||||
|
||||
## 验证
|
||||
|
||||
`#/browser` 与服务器「站点」按钮;禁止嵌入站点用新标签打开。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 审计 — 文件管理 chmod 不生效修复
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/application/services/files_browse_service.py` | ETag 含 permissions/owner/group |
|
||||
| `server/api/sync_v2.py` | chmod/chown 分步执行 |
|
||||
| `frontend/src/components/FilePermissionDialog.vue` | 仅变更时提交 owner/group |
|
||||
| `frontend/src/stores/files.ts` | 不继承旧 etag |
|
||||
| `tests/test_files_browse_etag.py` | 权限变更 etag 测试 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 304 不返回过期权限 | PASS — ETag 含 permissions |
|
||||
| H2 | chmod 不被 chown 连带失败 | PASS — 分步执行 |
|
||||
| H3 | CUD 审计保留 | PASS — `_audit_sync` 未删 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | compute_browse_etag |
|
||||
| H2 | PASS | sync_v2 chmod_file |
|
||||
| H3 | PASS | audit_bits 仍在 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest 14 passed
|
||||
- [x] type-check 通过
|
||||
- [x] changelog
|
||||
|
||||
## 验证
|
||||
|
||||
文件页改 644→755 → 列表权限列更新;仅改 mode 不填 chown 时不发 owner 字段。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 审计 — 文件管理上传后自动 www 权限
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/utils/files_upload_permissions.py` | 解析 RSYNC_PUSH 配置,SSH chown/chmod |
|
||||
| `server/api/sync_v2.py` | `_sftp_upload_one` 上传后 fixup + 响应/审计 |
|
||||
| `frontend/src/composables/files/useFilesActions.ts` | 权限修正失败 warning |
|
||||
| `tests/test_files_upload_permissions.py` | 解析与 apply 单测 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 复用推送权限配置,不硬编码 | PASS — `RSYNC_PUSH_CHOWN/CHMOD` |
|
||||
| H2 | 上传成功不因 fixup 失败回滚 | PASS — 仅返回 `permission_fixup.error` |
|
||||
| H3 | 命令注入防护 | PASS — `_RSYNC_*_RE` + `shlex.quote` |
|
||||
| H4 | CUD 审计 | PASS — audit_detail 含 chown/chmod |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | `upload_permission_plan()` |
|
||||
| H2 | PASS | sync_v2 不 raise on fixup fail |
|
||||
| H3 | PASS | sync_engine_v2 校验 + quote |
|
||||
| H4 | PASS | `_audit_sync("file_upload", ...)` |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest `test_files_upload_permissions.py` 7 passed
|
||||
- [x] changelog `2026-06-11-files-upload-www-permissions.md`
|
||||
|
||||
## 验证
|
||||
|
||||
文件页上传 → 远端 `ls -la` 为 `www www`;sudo 白名单不足时界面 warning、文件仍保留。
|
||||
@@ -0,0 +1,45 @@
|
||||
# 审计 — 全局浮动浏览器
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/utils/browser_ui_state.py` | URL 校验、visit 上限 |
|
||||
| `server/api/browser.py` | `/api/browser/state` |
|
||||
| `server/main.py` | 注册 browser 路由 |
|
||||
| `frontend/src/composables/useGlobalBrowser.ts` | 全局状态 |
|
||||
| `frontend/src/components/GlobalBrowserPanel.vue` | 浮动面板 |
|
||||
| `frontend/src/composables/useFloatingPanel.ts` | 可配置最小尺寸,支持迷你条拖动 |
|
||||
| `frontend/src/pages/BrowserPage.vue` | 打开全局窗并返回 |
|
||||
| `frontend/src/App.vue` | 挂载 GlobalBrowserPanel |
|
||||
| `frontend/src/pages/ServersPage.vue` | 站点按钮 |
|
||||
| `frontend/src/composables/useEmbeddedBrowser.ts` | 删除(由 global 替代) |
|
||||
| `tests/test_browser_ui_state.py` | 单测 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 无 SSRF | PASS — 无服务端 fetch |
|
||||
| H2 | URL 白名单 | PASS — http/https only |
|
||||
| H3 | 按管理员隔离 | PASS — admin_ui_preferences |
|
||||
| H4 | iframe sandbox | PASS |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | 纯 iframe,无 proxy |
|
||||
| H2 | PASS | browser_ui_state._is_safe_url |
|
||||
| H3 | PASS | AdminUiPreference per admin |
|
||||
| H4 | PASS | GlobalBrowserPanel sandbox attr |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest test_browser_ui_state.py
|
||||
- [x] type-check
|
||||
- [x] changelog
|
||||
|
||||
## 验证
|
||||
|
||||
浮动打开 → 最小化可拖动浮动条 → 左下角展开 → 不可关闭仅可重启 → 切换菜单 iframe 仍在。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 审计 — 未设路径推送回退 /www/wwwroot
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/utils/posix_paths.py` | `resolve_push_target_path`、默认 `/www/wwwroot` |
|
||||
| `server/application/services/sync_engine_v2.py` | 推送/预览使用 resolve |
|
||||
| `scripts/verify_push_elevation.py` | 验证脚本对齐 |
|
||||
| `scripts/batch_detect_target_path.py` | 批量 detect-path |
|
||||
| `scripts/batch_detect_target_path.sh` | 包装入口 |
|
||||
| `tests/test_posix_paths.py` | unset UI 与 push 回退分离单测 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 未改 `is_unset_target_path` UI 判定 | PASS |
|
||||
| H2 | 推送 override 仍优先 | PASS — resolve 先 override |
|
||||
| H3 | 路径校验仍走 normalize_remote_abs_path | PASS |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | is_unset_target_path 未动 |
|
||||
| H2 | PASS | resolve_push_target_path |
|
||||
| H3 | PASS | posix_paths.py |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest test_posix_paths
|
||||
- [x] changelog 2026-06-11-push-default-wwwroot.md
|
||||
- [x] batch_detect_target_path.sh
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
pytest tests/test_posix_paths.py -q
|
||||
bash scripts/batch_detect_target_path.sh --unset-only --dry-run
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# 审计 — 移除内置浏览器 + 推送页服务器 UI
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-11-remove-embedded-browser.md` · `docs/changelog/2026-06-11-push-server-name-display.md`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/App.vue` | 移除浏览器侧栏与 GlobalBrowserPanel |
|
||||
| `frontend/src/router/index.ts` | 移除 `/browser` |
|
||||
| `frontend/src/pages/ServersPage.vue` | 站点 → 新标签打开 |
|
||||
| `frontend/src/components/GlobalBrowserPanel.vue` | 删除 |
|
||||
| `frontend/src/composables/useGlobalBrowser.ts` | 删除 |
|
||||
| `frontend/src/pages/BrowserPage.vue` | 删除 |
|
||||
| `server/api/browser.py` | 删除 |
|
||||
| `server/utils/browser_ui_state.py` | 删除 |
|
||||
| `server/main.py` | 注销 browser 路由 |
|
||||
| `frontend/src/components/push/PushServerGrid.vue` | 卡片/列表切换、名称复制 |
|
||||
| `frontend/src/composables/push/usePushServerViewMode.ts` | 视图偏好 localStorage |
|
||||
| `frontend/src/components/push/PushProgressList.vue` | 名称 truncate + title |
|
||||
| `frontend/src/components/push/PushHistoryTable.vue` | 服务器列 slot |
|
||||
| `frontend/src/components/push/PushPreviewDialog.vue` | 名称 truncate + title |
|
||||
| `frontend/src/composables/push/usePushLogs.ts` | 列宽 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | SSRF / 安全 | PASS — 删除 iframe 浏览器 API;推送页无新后端 |
|
||||
| H2 | 无静默吞错 | PASS |
|
||||
| H3 | clipboard | PASS — copy 失败有 snackbar |
|
||||
| H4 | CUD 审计 | N/A — 删除 browser state API |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | 前端 UI + 删除 dead code |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `npm run type-check`
|
||||
- [x] `import server.main`
|
||||
- [ ] 生产 `/app/#/push` 验证列表切换与复制
|
||||
|
||||
## 验证
|
||||
|
||||
侧栏无浏览器;推送页卡片/列表切换;点击名称复制。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 审计 — 移除 iframe 内置浏览器
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-11-remove-embedded-browser.md`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/components/GlobalBrowserPanel.vue` | 删除 |
|
||||
| `frontend/src/composables/useGlobalBrowser.ts` | 删除 |
|
||||
| `frontend/src/pages/BrowserPage.vue` | 删除 |
|
||||
| `server/api/browser.py` | 删除 |
|
||||
| `server/utils/browser_ui_state.py` | 删除 |
|
||||
| `tests/test_browser_ui_state.py` | 删除 |
|
||||
| `frontend/src/App.vue` | 移除 GlobalBrowserPanel、侧栏项 |
|
||||
| `frontend/src/router/index.ts` | 移除路由 |
|
||||
| `frontend/src/pages/ServersPage.vue` | 站点 → window.open |
|
||||
| `server/main.py` | 移除 router |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 无 SSRF 新增 | PASS — 删除 iframe,站点按钮仅客户端 window.open |
|
||||
| H2 | 无静默吞错 | PASS — 无新增 |
|
||||
| H3 | 无密钥 | PASS |
|
||||
| H4 | CUD 审计 | N/A — 删除 API,无新 CUD |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | 纯删除 + 外链兜底 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] type-check
|
||||
- [x] 删除 `/api/browser` 路由
|
||||
- [ ] 生产部署(待用户批准)
|
||||
|
||||
## 验证
|
||||
|
||||
侧栏无浏览器;站点按钮新标签打开公网 URL。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 审计 — rsync 推送 sudo 提权 + 批量 sudoers 补丁
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/utils/rsync_elevation.py` | 非 root 默认 sudo rsync |
|
||||
| `server/application/services/sync_engine_v2.py` | `_rsync_push` 接入 |
|
||||
| `server/application/services/files_sudoers_service.py` | sudoers 模板与安装 |
|
||||
| `server/api/sync_v2.py` | 诊断写入 sudo 回退 |
|
||||
| `server/api/servers.py` | setup-files-sudo 复用 service |
|
||||
| `scripts/batch_patch_files_sudoers.py` | 批量补丁脚本 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | sudoers 无用户注入 | PASS — `build_nexus_files_sudoers` 固定白名单 |
|
||||
| H2 | rsync-path 无外部输入 | PASS — 常量 `sudo -n rsync` |
|
||||
| H3 | 密码不经 argv | PASS — stdin `sudo -S` |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | files_sudoers_service |
|
||||
| H2 | PASS | rsync_elevation.py |
|
||||
| H3 | PASS | _write_sudoers_with_password |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest test_rsync_elevation + test_nexus_files_sudoers
|
||||
- [x] changelog 2026-06-11-rsync-push-sudo-elevation.md
|
||||
- [x] 批量脚本 batch_patch_files_sudoers.sh
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
pytest tests/test_rsync_elevation.py tests/test_nexus_files_sudoers.py -q
|
||||
bash scripts/batch_patch_files_sudoers.sh --name 温胜夜 --dry-run
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
# 审计 — 新服务器接入自动化
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/application/services/server_onboarding_service.py` | 调度入口 |
|
||||
| `server/application/services/server_batch_service.py` | `_run_onboard` |
|
||||
| `server/application/server_batch_common.py` | `detect_and_save_target_path` |
|
||||
| `server/api/servers.py` | 创建/导入/IP 添加钩子 |
|
||||
| `server/api/schemas.py` | `onboarding_job_id` |
|
||||
| `frontend/src/composables/servers/useServerFormDialog.ts` | 任务注册 |
|
||||
| `tests/test_server_onboarding.py` | 单测 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | sudoers 仍走白名单模板 | PASS |
|
||||
| H2 | 创建 API 不因 onboard 失败而 5xx | PASS — 后台 job |
|
||||
| H3 | 探测失败不阻断推送 | PASS — wwwroot 回退 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | files_sudoers_service |
|
||||
| H2 | PASS | schedule 独立 try/except |
|
||||
| H3 | PASS | detect 失败 stdout 提示 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest test_server_onboarding
|
||||
- [x] changelog 2026-06-11-server-onboarding-automation.md
|
||||
- [x] 设计 docs/design/specs/2026-06-11-server-onboarding-automation-design.md
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
pytest tests/test_server_onboarding.py -q
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
# 审计 — 服务器搜索历史 MySQL 持久化
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/domain/models/__init__.py` | 新增 `AdminUiPreference` |
|
||||
| `server/infrastructure/database/admin_ui_preference_repo.py` | 读写偏好 JSON |
|
||||
| `server/infrastructure/database/migrations.py` | 建表 `admin_ui_preferences` |
|
||||
| `server/utils/server_search_history.py` | 去重/上限 12 条 |
|
||||
| `server/api/servers.py` | `GET/PUT /api/servers/search-history` |
|
||||
| `server/api/schemas.py` | `ServerSearchHistoryUpdate` |
|
||||
| `frontend/src/composables/useServerSearchHistory.ts` | API + localStorage 一次性迁移 |
|
||||
| `frontend/src/pages/ServersPage.vue` | combobox 搜索历史 |
|
||||
| `tests/test_server_search_history.py` | 单元 + API 往返 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 路由在 `/{id}` 之前注册 | PASS — `search-history` 静态路径优先 |
|
||||
| H2 | 仅当前管理员可读写 | PASS — `get_current_admin` + `admin_id` |
|
||||
| H3 | 无静默吞错 | PASS — API 异常向上抛出 |
|
||||
| H4 | 无跨层直连 SQL | PASS — 经 `admin_ui_preference_repo` |
|
||||
| H5 | 输入校验 | PASS — Pydantic + 归一化 strip/上限 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | `servers.py` 路由顺序 |
|
||||
| H2 | PASS | repo 按 `admin_id` + `context` |
|
||||
| H3 | PASS | 无 bare except |
|
||||
| H4 | PASS | 分层合规 |
|
||||
| H5 | PASS | `server_search_history.py` |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `pytest tests/test_server_search_history.py` 4 passed
|
||||
- [x] `npm run type-check` 通过
|
||||
- [x] changelog `2026-06-11-servers-search-history.md`
|
||||
- [x] migration 启动时建表
|
||||
|
||||
## 验证
|
||||
|
||||
Servers 页搜索 → 刷新 → 历史仍在;换浏览器同账号 → GET 返回相同 `history`/`last`。
|
||||
@@ -0,0 +1,36 @@
|
||||
# 审计 — Servers 搜索手动触发与 null 修复
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/composables/useServerSearchHistory.ts` | 仅加载历史、不恢复 last;record null 安全 |
|
||||
| `frontend/src/composables/useServerPagination.ts` | search null 安全 trim |
|
||||
| `frontend/src/pages/ServersPage.vue` | searchDraft + 回车/下拉/清空才搜索 |
|
||||
| `server/utils/validation_errors_zh.py` | 422 剥离不可序列化 ctx |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 进页不自动过滤 | PASS — refresh 重置 search |
|
||||
| H2 | 无静默吞错 | PASS — record/load 失败非阻塞 |
|
||||
| H3 | 422 可 JSON 序列化 | PASS — ctx 不写入响应 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | loadHistoryOnly 不写 search |
|
||||
| H2 | PASS | try/catch 保留 |
|
||||
| H3 | PASS | translate_validation_errors |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] npm run type-check
|
||||
- [x] changelog 2026-06-11-servers-search-manual-trigger.md
|
||||
- [x] 进页全量列表;手动搜索才请求
|
||||
|
||||
## 验证
|
||||
|
||||
Servers 页进入 → 无搜索 chip;下拉选历史或回车 → 过滤;清空 → 全量。
|
||||
@@ -0,0 +1,117 @@
|
||||
# 审计 — 监测槽开关 / 8h·24h / 暂停占槽 / 探针中文错误
|
||||
|
||||
**日期**:2026-06-11
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-11-watch-slot-monitoring-toggle.md`
|
||||
- `docs/changelog/2026-06-11-watch-probe-parse-error-fix.md`
|
||||
|
||||
## 范围(未提交工作区,+799 / −124 行)
|
||||
|
||||
| 层级 | 文件 |
|
||||
|------|------|
|
||||
| 模型/迁移 | `server/domain/models/__init__.py`(`monitoring_enabled`、`ttl_hours`) |
|
||||
| | `server/infrastructure/database/migrations.py` |
|
||||
| Repository | `server/infrastructure/database/watch_repo.py` |
|
||||
| Service | `server/application/services/watch_service.py` |
|
||||
| API | `server/api/watch.py`(PATCH monitoring / ttl) |
|
||||
| 探针 | `server/background/watch_probe_runner.py` |
|
||||
| | `server/infrastructure/ssh/remote_probe.py` |
|
||||
| 工具 | `server/utils/watch_metrics.py` |
|
||||
| | `server/utils/watch_state.py`(**新增** Redis 清理) |
|
||||
| | `server/utils/watch_probe_errors.py`(**新增** 中文错误) |
|
||||
| 前端 | `frontend/src/composables/useWatchPins.ts` |
|
||||
| | `frontend/src/components/watch/WatchSlotCard.vue` |
|
||||
| | `frontend/src/components/watch/WatchSlotRow.vue` |
|
||||
| | `frontend/src/components/watch/WatchProbeRecordsTable.vue` |
|
||||
| | `frontend/src/utils/watchFormat.ts` |
|
||||
| | `frontend/src/api/index.ts`(`http.patch`) |
|
||||
| 测试 | `tests/test_watch_pins.py` |
|
||||
| | `tests/test_watch_metrics.py` |
|
||||
| | `tests/test_watch_probe_errors.py`(**新增**) |
|
||||
|
||||
## 功能摘要
|
||||
|
||||
1. **实时监测开关**:`monitoring_enabled`;关则停 5s 探针,槽位保留
|
||||
2. **8h / 24h**:`ttl_hours` + `PATCH /pins/{slot}/ttl`;默认 8h,显式切 24h
|
||||
3. **暂停永久占槽**:`monitoring_enabled=false` 时不参与 TTL 过期删除
|
||||
4. **时长到期**:自动暂停(等同关开关),结束 session,**不删 pin**
|
||||
5. **恢复 Agent 60s**:无活跃监测时清理 `watch:live/proc/last`;Agent 心跳逻辑未改
|
||||
6. **parse_error 修复**:Agent 有 CPU/内存时 SSH 失败不拖垮整轮;SSH 脚本加固
|
||||
7. **错误中文**:后端写入 + 前端 `formatProbeError` 兼容历史英文
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 鉴权 / IDOR | **PASS** — 新 PATCH 均 `get_current_admin`;repo 带 `admin_id` |
|
||||
| H2 | 无静默吞错 | **PASS** — 探针失败仍落库;Redis 清理失败会抛错(生产有 Redis) |
|
||||
| H3 | 无新密钥 | **PASS** |
|
||||
| H4 | CUD 审计 | **PASS** — `watch_pin_pause` / `watch_pin_resume` / `watch_pin_ttl` / remove |
|
||||
| H5 | 输入边界 | **PASS** — `Literal[8,24]`、`slot_index` 0–3 |
|
||||
| H6 | 迁移 | **PASS** — ALTER + duplicate column 容错 |
|
||||
| H7 | 产品风险 | **RISK 接受** — 4 槽均可永久暂停占用,需手动 X 释放(用户明确要求) |
|
||||
|
||||
## 逐模块走读
|
||||
|
||||
### 后端
|
||||
|
||||
| 模块 | 结论 | 备注 |
|
||||
|------|------|------|
|
||||
| `watch_repo._pin_still_occupied` | PASS | 暂停不占 TTL;开启监测才看过期 |
|
||||
| `expire_due_pins` | PASS | 改为暂停 + end session,不 delete pin |
|
||||
| `set_monitoring_enabled` + `_ensure_open_session` | PASS | TTL 到期后再开自动新 session |
|
||||
| `_restore_idle_agent_watch` | PASS | 全局无监测才清 Redis |
|
||||
| `replace_slot` | PASS | 保留槽位原 `ttl_hours`(审查项已修) |
|
||||
| `probe_server_metrics` | PASS | Agent 基础指标优先;SSH 仅补 IO |
|
||||
| `watch_probe_error_zh` | PASS | 与前端映射一致 |
|
||||
|
||||
### 前端
|
||||
|
||||
| 模块 | 结论 | 备注 |
|
||||
|------|------|------|
|
||||
| `WatchSlotCard` | PASS | 开关 + 8h/24h + 暂停态 UI |
|
||||
| `tickCountdown` | PASS | 归零 `refreshPins`,不再误置 `empty` |
|
||||
| `formatProbeError` | PASS | 状态/错误/来源中文化 |
|
||||
| `WatchProbeRecordsTable` | PASS | 筛选器与列中文 |
|
||||
|
||||
## 审计中发现并修复
|
||||
|
||||
| 项 | 严重度 | 处理 |
|
||||
|----|--------|------|
|
||||
| `mock_watch_redis` 未 patch `watch_state.get_redis` | **P1** | 已修:`remove_slot` / `set_slot_monitoring` 单测 2 失败 → 19 passed |
|
||||
|
||||
## 遗留建议(非阻塞)
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 设计文档 | 开关/暂停语义变更较大,建议补 `docs/design/specs/` 短文(当前仅 changelog) |
|
||||
| HTTP 集成测试 | PATCH monitoring/ttl 的 404/422 路径未覆盖 |
|
||||
| TTL 到期审计 | 后台 `expire_due_pins` 无 audit_log(可接受,session.end_reason=expired 可查) |
|
||||
| PATCH 404 语义 | 「槽位无效」与「槽位为空」均 404,与 POST 422 不一致 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H6 | **PASS** | 鉴权、审计、迁移、探针落库均符合铁律 |
|
||||
| H7 | **RISK 接受** | 用户要求暂停永久占槽 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `.venv/bin/pytest tests/test_watch_metrics.py tests/test_watch_pins.py tests/test_watch_probe_errors.py` — **19 passed**
|
||||
- [x] `cd frontend && npm run type-check`
|
||||
- [x] 审计修复:test fixture patch `watch_state.get_redis`
|
||||
- [ ] `bash scripts/local_verify.sh` / 门控 7 道(部署前)
|
||||
- [ ] 生产浏览器:开关暂停、24h 切换、到期自动暂停、中文错误展示
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
.venv/bin/pytest tests/test_watch_metrics.py tests/test_watch_pins.py tests/test_watch_probe_errors.py -q
|
||||
cd frontend && npm run type-check
|
||||
bash deploy/pre_deploy_check.sh
|
||||
```
|
||||
|
||||
## 合并建议
|
||||
|
||||
**Approve with notes** — 功能完整、测试绿、安全与审计达标;部署前跑门控 + 浏览器终验。遗留 HTTP 集成测试与设计文档可跟进 PR。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 审计 — 审计日志目标列显示服务器名称
|
||||
|
||||
**日期**:2026-06-12
|
||||
**Changelog**: `docs/changelog/2026-06-12-audit-log-server-target-name.md`
|
||||
|
||||
## 范围
|
||||
|
||||
| 文件 |
|
||||
|------|
|
||||
| `server/api/settings.py` |
|
||||
| `frontend/src/pages/AuditPage.vue` |
|
||||
| `frontend/src/utils/auditLabels.ts` |
|
||||
| `frontend/src/utils/auditNormalize.ts` |
|
||||
| `frontend/src/types/api.ts` |
|
||||
| `tests/integration/test_alerts_audit.py` |
|
||||
|
||||
## Step 3 / Closure / DoD
|
||||
|
||||
- PASS — 只读增强,批量 IN 查 name
|
||||
- DoD: integration test passed
|
||||
@@ -0,0 +1,84 @@
|
||||
# 审计 — 文件管理服务器搜索 + 监测槽点击修复
|
||||
|
||||
**日期**:2026-06-12
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-11-files-server-search.md`(commit `6140d68`)
|
||||
- `docs/changelog/2026-06-11-watch-slot-click-fix.md`(commit `6b8965a`)
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/components/files/FilesToolbar.vue` | v-select → v-autocomplete 可搜索选服 |
|
||||
| `frontend/src/composables/files/useFilesNavigation.ts` | 全量 loadServers、filteredServerList |
|
||||
| `frontend/src/composables/files/useFilesPage.ts` | 暴露搜索状态 |
|
||||
| `frontend/src/components/watch/WatchSlotCard.vue` | 空槽/满槽点击、hover |
|
||||
| `frontend/src/components/watch/WatchSlotPickDialog.vue` | 空槽选机对话框(新增) |
|
||||
| `frontend/src/components/watch/WatchSlotRow.vue` | 串联 pick 与 pinServer |
|
||||
| `frontend/src/components/watch/WatchSparkline.vue` | pointer-events 防挡点击 |
|
||||
| `frontend/src/composables/useWatchPins.ts` | pinServer 支持 slot_index |
|
||||
| `frontend/src/pages/ServersPage.vue` | 已在监测 + 提示 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 鉴权 / IDOR | PASS — `/servers/`、`/watch/pins` 均走 JWT;Pin 按 admin 隔离(后端已有) |
|
||||
| H2 | 无静默吞错 | PASS(审计中修复)— WatchSlotPickDialog 加载失败改 snackbar;pin 失败经 formatApiError |
|
||||
| H3 | 无新密钥 | PASS — 纯前端 UI |
|
||||
| H4 | XSS | PASS — 无 v-html;服务器名由 API 返回、Vuetify 文本渲染 |
|
||||
| H5 | 输入边界 | PASS — serverId/slotIndex 为 number;后端 Pydantic ge/le 校验 |
|
||||
| H6 | 性能 | RISK 接受 — 全量拉取 servers(与终端/推送一致);2000+ 台客户端 filter |
|
||||
|
||||
## 逐行走读(前端 5 文件)
|
||||
|
||||
| 文件 | 行数 | Read | 结论 |
|
||||
|------|------|------|------|
|
||||
| WatchSlotPickDialog.vue | 108 | 1–108 | 逐行完成 ✓ |
|
||||
| WatchSlotCard.vue | 138 | 1–138 | 逐行完成 ✓ |
|
||||
| WatchSlotRow.vue | 98 | 1–98 | 逐行完成 ✓ |
|
||||
| useWatchPins.ts(pinServer 段) | 222 | 160–178 | 逐行完成 ✓ |
|
||||
| useFilesNavigation.ts(搜索段) | 244 | 26–47,139–170 | 逐行完成 ✓ |
|
||||
|
||||
### Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | SAFE | fetchPagePerPage/http 带 auth;POST body 仅 server_id + slot_index |
|
||||
| H2 | SAFE | 选机对话框 load 失败 snackbar;onPickServer 409/422 均有 snackbar |
|
||||
| H3 | SAFE | 无 env/密钥 |
|
||||
| H4 | SAFE | 模板无 innerHTML |
|
||||
| H5 | SAFE | slot_index 0–3 后端校验;空 selectedId 禁用「加入」 |
|
||||
| H6 | SAFE(接受) | 与项目内终端/推送全量列表模式一致 |
|
||||
|
||||
### 交互验收
|
||||
|
||||
| 场景 | 预期 | 审计结论 |
|
||||
|------|------|----------|
|
||||
| 文件页搜索服务器 | 名称/域名/路径/ID 过滤 | PASS(type-check) |
|
||||
| 点击虚线空槽 | 弹出选机 → POST slot_index | PASS(代码路径完整) |
|
||||
| 点击满槽卡片 | 路由 WatchMetrics + query | PASS |
|
||||
| 列表 + 已 Pin | snackbar 槽位号 | PASS |
|
||||
| 满 4 槽替换对话框 | replace_slot POST | PASS(既有逻辑未改坏) |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `pytest tests/test_watch_metrics.py tests/test_watch_pins.py` — 10 passed
|
||||
- [x] `cd frontend && npm run type-check`
|
||||
- [x] 审计修复:WatchSlotPickDialog 加载失败 snackbar
|
||||
- [x] 生产已部署 `6b8965a`(监测槽点击);`6140d68`(文件搜索)
|
||||
- [ ] Gate 3 test_api.py — 需本地 API :8600 运行(审计时未起服务)
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
.venv/bin/pytest tests/test_watch_metrics.py tests/test_watch_pins.py -q
|
||||
cd frontend && npm run type-check
|
||||
bash scripts/local_verify.sh # 含 Gate 3
|
||||
bash deploy/pre_deploy_check.sh
|
||||
```
|
||||
|
||||
## 备注
|
||||
|
||||
- 监测槽空槽点击为设计文档明确要求(`2026-06-12-server-watch-slots-design.md` §信息架构),原实现遗漏,已补。
|
||||
- 无后端变更;无需迁移/重启 API(仅前端)。
|
||||
@@ -0,0 +1,17 @@
|
||||
# 审计 — psutil 安装 apt sudo 修复
|
||||
|
||||
**日期**:2026-06-12
|
||||
**Changelog**: `docs/changelog/2026-06-12-psutil-install-sudo-fix.md`
|
||||
|
||||
## 范围
|
||||
|
||||
| 文件 |
|
||||
|------|
|
||||
| `server/utils/psutil_install.py` |
|
||||
| `server/application/server_batch_common.py` |
|
||||
| `tests/test_psutil_install.py` |
|
||||
|
||||
## Step 3 / Closure / DoD
|
||||
|
||||
- H1 PASS — 行为修复,无新 API
|
||||
- DoD: pytest test_psutil_install 5 passed
|
||||
@@ -0,0 +1,33 @@
|
||||
# 审计 — 推送页移除点击复制名称
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-12-push-remove-name-copy.md`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/components/push/PushServerGrid.vue` | 移除名称点击复制 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 安全 | PASS — 无新 API |
|
||||
| H2 | 无静默吞错 | PASS |
|
||||
| H3 | 无新密钥 | PASS |
|
||||
| H4 | CUD | N/A |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | 纯前端交互回退 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] type-check
|
||||
- [ ] 生产 `#/push` 验证
|
||||
|
||||
## 验证
|
||||
|
||||
点击名称仅勾选服务器,无复制 toast。
|
||||
@@ -0,0 +1,46 @@
|
||||
# 审计 — 推送搜索历史 + 终端搜索回中间
|
||||
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-12-push-server-search-history.md`
|
||||
- `docs/changelog/2026-06-12-terminal-search-center-empty-state.md`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/utils/server_search_history.py` | push 上下文,上限 5 |
|
||||
| `server/api/servers.py` | push-search-history API |
|
||||
| `frontend/src/composables/useServerSearchHistory.ts` | scope 参数 |
|
||||
| `frontend/src/composables/push/usePushServers.ts` | 推送搜索历史 |
|
||||
| `frontend/src/composables/push/usePushPage.ts` | 加载历史 |
|
||||
| `frontend/src/components/push/PushServerGrid.vue` | combobox |
|
||||
| `frontend/src/pages/PushPage.vue` | 绑定历史 |
|
||||
| `frontend/src/pages/TerminalPage.vue` | 中间空态搜索 |
|
||||
| `frontend/src/components/terminal/TerminalArea.vue` | empty-picker 插槽 |
|
||||
| `frontend/src/components/terminal/TerminalServerPicker.vue` | 侧栏无搜索 |
|
||||
| `tests/test_server_search_history.py` | push 上限与 API |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 安全 | PASS — 按 admin 隔离偏好 |
|
||||
| H2 | 无静默吞错 | PASS |
|
||||
| H3 | 无新密钥 | PASS |
|
||||
| H4 | CUD | N/A — 仅 UI 偏好 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | 复用 admin_ui_preferences |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] type-check
|
||||
- [x] pytest test_server_search_history
|
||||
- [ ] 生产 `#/push` / `#/terminal` 验证
|
||||
|
||||
## 验证
|
||||
|
||||
推送:combobox 最近 5 条。终端:无会话时中间搜索,右侧仅列表。
|
||||
@@ -0,0 +1,142 @@
|
||||
# 审计 — 远程浏览器(Playwright Worker)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-12-remote-browser-worker-deploy.md`
|
||||
**设计 SSOT**: `docs/design/specs/2026-06-11-server-browser-chromium-design.md`
|
||||
**范围**: 2026-06-12 新增/恢复的浏览器 Worker 全链路(未含 `web/app` 构建产物)
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `browser-worker/Dockerfile` | Worker 镜像 |
|
||||
| `browser-worker/docker-compose.yml` | Worker 编排 |
|
||||
| `browser-worker/docker-entrypoint.sh` | headed + Xvfb 入口 |
|
||||
| `browser-worker/requirements.txt` | Worker 依赖 |
|
||||
| `browser-worker/config.py` | Worker 环境配置 |
|
||||
| `browser-worker/main.py` | Worker HTTP/WS API |
|
||||
| `browser-worker/session_manager.py` | Playwright + screencast |
|
||||
| `browser-worker/browser_stealth.py` | 反自动化 / 验证码辅助 |
|
||||
| `browser-worker/browser_url_safe.py` | SSRF(Worker 侧) |
|
||||
| `server/main.py` | 注册 browser 路由 |
|
||||
| `server/config.py` | `BROWSER_*` 与 TLS verify |
|
||||
| `server/api/settings.py` | 设置项默认值(push_complete_sound) |
|
||||
| `server/api/browser_session.py` | 桥接 REST + `/ws/browser` |
|
||||
| `server/api/browser.py` | UI 状态 `/api/browser/state` |
|
||||
| `server/infrastructure/browser/__init__.py` | 包入口 |
|
||||
| `server/infrastructure/browser/worker_client.py` | 连 Worker(可配置 TLS verify) |
|
||||
| `server/infrastructure/browser/session_registry.py` | 内存会话归属 |
|
||||
| `server/utils/browser_url_safe.py` | SSRF(桥接层) |
|
||||
| `server/utils/browser_ui_state.py` | 状态解析 |
|
||||
| `frontend/src/App.vue` | 顶栏 + 固定 GlobalBrowserPanel |
|
||||
| `frontend/src/router/index.ts` | `/browser` 路由 |
|
||||
| `frontend/src/pages/ServersPage.vue` | 服务器页打开浏览器 |
|
||||
| `frontend/src/pages/BrowserPage.vue` | 独立浏览器页(兼容) |
|
||||
| `frontend/src/components/GlobalBrowserPanel.vue` | 固定顶栏下整窗 |
|
||||
| `frontend/src/composables/useGlobalBrowser.ts` | 全局标签/状态 |
|
||||
| `frontend/src/composables/useRemoteBrowserSession.ts` | canvas + WS |
|
||||
| `tests/test_browser_url_safe.py` | SSRF 单测 |
|
||||
| `tests/test_browser_ui_state.py` | UI 状态单测 |
|
||||
| `tests/test_settings_sound_defaults.py` | 设置默认音单测 |
|
||||
|
||||
## API / WS 入口表
|
||||
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| GET | `/api/browser/status` | JWT `get_current_admin` |
|
||||
| POST | `/api/browser/sessions` | JWT |
|
||||
| DELETE | `/api/browser/sessions/{id}` | JWT + 会话归属 |
|
||||
| GET/PUT | `/api/browser/state` | JWT + `admin_ui_preferences` |
|
||||
| WS | `/ws/browser/{session_id}?token=` | JWT + 会话归属 |
|
||||
| GET | Worker `/health` | **无** |
|
||||
| POST/DELETE | Worker `/v1/sessions*` | `X-Nexus-Worker-Key` |
|
||||
| WS | Worker `/v1/sessions/{id}/stream` | Worker Key(Header) |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| ID | 规则 | 结论 |
|
||||
|----|------|------|
|
||||
| H1 | SSRF / 私网访问 | **RISK** — 见 F1、F2 |
|
||||
| H2 | 鉴权 / IDOR | PASS — WS 校验 admin_id |
|
||||
| H3 | 密钥不落库/不入 git | PASS — `.env` / 持久卷 |
|
||||
| H4 | 无静默吞错 | **RISK** — 见 F4 |
|
||||
| H5 | Worker 暴露面 | **RISK** — 见 F3 |
|
||||
| H6 | 多 worker 一致性 | **RISK** — 见 F5 |
|
||||
| H7 | UI 状态 URL 白名单 | PASS — `browser_ui_state` |
|
||||
| H8 | 设计符合度(redirect) | **GAP** — 见 F1 |
|
||||
|
||||
## Closure(全表)
|
||||
|
||||
| ID | 判定 | 严重度 | 位置 | 说明 |
|
||||
|----|------|--------|------|------|
|
||||
| H1 | FINDING | **P1** | `browser-worker/session_manager.py` NAVIGATE 后 | **F1** 页内点击/重定向未逐跳 SSRF 校验;设计文档要求 redirect 后重验 |
|
||||
| H1 | FINDING | **P1** | Worker 部署 ufw | **F3** 装机时额外 `ufw allow 8443/tcp`(全网),削弱「仅主站 IP」隔离 |
|
||||
| H2 | SAFE | — | `browser_session.py:125-127` | `session_id` 必须属于当前 `admin.id` |
|
||||
| H2 | SAFE | — | `browser_session.py:95-104` | 桥接层 NAVIGATE 二次校验 |
|
||||
| H3 | SAFE | — | `worker_client.py`, Worker `.env` | `WORKER_SECRET` 仅环境变量 |
|
||||
| H4 | FINDING | P2 | `worker_client.py:67-68` | **F4** `delete_worker_session` 网络失败仅 warning,可能残留 Worker 会话 |
|
||||
| H5 | FINDING | P2 | `browser-worker/main.py:45-51` | `/health` 无认证;若 8443 对公网可见会泄露会话数 |
|
||||
| H6 | FINDING | P2 | `session_registry.py` | **F5** 内存注册表不跨 uvicorn worker;多进程下 WS 可能 4403 |
|
||||
| H7 | SAFE | — | `browser_ui_state.py` | http(s) only,无凭据 URL |
|
||||
| H8 | SAFE | — | screencast 热修 | `cdp.on("Page.screencastFrame")` 已替代错误 API |
|
||||
| — | SAFE | — | `browser_session.py:61-64` | 创建前清理陈旧会话,避免 409 |
|
||||
| — | SAFE | — | 前端 | 不再 iframe;canvas 远程渲染 |
|
||||
| — | SAFE | — | CUD 审计 | 首版未记 session 审计,与设计「与 RDP 一致」一致 |
|
||||
|
||||
### F1 — 重定向 / 页内导航 SSRF 缺口(P1)
|
||||
|
||||
`validate_navigation_url` 仅在显式 `NAVIGATE` 消息时调用。用户在远程 Chromium 内**点击链接**或 **302 跳转**到 `http://10.x` / metadata 时,Playwright 会照常请求,**不经过**校验。
|
||||
|
||||
**建议**:`page.on("framenavigated")` / `response` 监听,每跳对 `page.url` 调用 `validate_navigation_url`;失败则 `page.close()` 或 `about:blank` 并 WS `ERROR`。
|
||||
|
||||
### F3 — Worker 防火墙过宽(P1)
|
||||
|
||||
部署日志显示除 `from 20.24.218.235` 外还有 `ufw allow 8443/tcp`(Anywhere)。攻击者若猜到 `WORKER_SECRET`(或暴力)可直连 Worker。
|
||||
|
||||
**建议**:删除全网 8443 规则,仅保留主站源 IP;`/health` 改内网或加 Key。
|
||||
|
||||
### F4 — Worker 删除失败(P2)
|
||||
|
||||
`delete_worker_session` 异常时静默;依赖 Worker 空闲超时清理。
|
||||
|
||||
### F5 — 多 uvicorn worker(P2)
|
||||
|
||||
`session_registry` 进程内字典;生产若 `--workers>1` 可能 POST 在 worker A、WS 落到 worker B。当前 Docker 单进程可接受;扩 worker 时需 Redis 注册表。
|
||||
|
||||
## 外部输入 → Sink
|
||||
|
||||
| 输入 | Sink | 缓解 |
|
||||
|------|------|------|
|
||||
| `NAVIGATE.url` | Playwright `goto` | 双端 `validate_navigation_url` |
|
||||
| 页内链接点击 | Playwright 导航 | **未缓解(F1)** |
|
||||
| WS `MOUSE`/`KEY` | Chromium | 仅已认证管理员 |
|
||||
| `browser/state` tabs URL | MySQL JSON | `_is_safe_url` 过滤 |
|
||||
| Worker Key | Worker API | 常量时间比较(`!=`) |
|
||||
|
||||
## DoD
|
||||
|
||||
| 项 | 状态 |
|
||||
|----|------|
|
||||
| `pytest tests/test_browser_url_safe.py tests/test_browser_ui_state.py` | 7 passed(生产机);本地沙箱 DNS 失败属环境限制 |
|
||||
| `npm run type-check` | 已通过(实现日) |
|
||||
| 生产 `GET /api/browser/status` | `enabled:true`, `worker_ok:true` |
|
||||
| 生产 screencast 冒烟 | `SCREENCAST_OK` ~4KB JPEG |
|
||||
| changelog | `docs/changelog/2026-06-12-remote-browser-worker-deploy.md` |
|
||||
|
||||
## 总结
|
||||
|
||||
| 级别 | 数量 | 处置建议 |
|
||||
|------|------|----------|
|
||||
| P0 | 0 | — |
|
||||
| P1 | 2 | F1 redirect/页内 SSRF;F3 收紧 ufw |
|
||||
| P2 | 3 | F4/F5 可排期;/health 加固 |
|
||||
|
||||
**合并结论**:功能链路可用,鉴权与显式 NAVIGATE SSRF 达标;**未达设计文档「每跳 redirect 校验」**,且 Worker **8443 应对公网收口**。建议先修 F3(运维),再实现 F1(代码)后复审计。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
venv/bin/pytest tests/test_browser_url_safe.py tests/test_browser_ui_state.py -q
|
||||
cd frontend && npm run type-check
|
||||
# 生产
|
||||
curl -H "Authorization: Bearer $TOKEN" https://api.synaglobal.vip/api/browser/status
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# 审计 — 移除内置浏览器 + 推送页服务器 UI
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-11-remove-embedded-browser.md` · `docs/changelog/2026-06-11-push-server-name-display.md`
|
||||
**Commit**: `5407125`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/App.vue` | 移除浏览器侧栏与 GlobalBrowserPanel |
|
||||
| `frontend/src/router/index.ts` | 移除 `/browser` |
|
||||
| `frontend/src/pages/ServersPage.vue` | 站点 → 新标签打开 |
|
||||
| `frontend/src/components/GlobalBrowserPanel.vue` | 删除 |
|
||||
| `frontend/src/composables/useGlobalBrowser.ts` | 删除 |
|
||||
| `frontend/src/pages/BrowserPage.vue` | 删除 |
|
||||
| `server/api/browser.py` | 删除 |
|
||||
| `server/utils/browser_ui_state.py` | 删除 |
|
||||
| `server/main.py` | 注销 browser 路由 |
|
||||
| `frontend/src/components/push/PushServerGrid.vue` | 卡片/列表切换、名称复制 |
|
||||
| `frontend/src/composables/push/usePushServerViewMode.ts` | 视图偏好 localStorage |
|
||||
| `frontend/src/components/push/PushProgressList.vue` | 名称 truncate + title |
|
||||
| `frontend/src/components/push/PushHistoryTable.vue` | 服务器列 slot |
|
||||
| `frontend/src/components/push/PushPreviewDialog.vue` | 名称 truncate + title |
|
||||
| `frontend/src/composables/push/usePushLogs.ts` | 列宽 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | SSRF / 安全 | PASS — 删除 iframe 浏览器 API |
|
||||
| H2 | 无静默吞错 | PASS |
|
||||
| H3 | clipboard | PASS — copy 失败有 snackbar |
|
||||
| H4 | CUD 审计 | N/A |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | 前端 UI + 删除 dead code |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] type-check · import server.main
|
||||
- [ ] 生产推送页验证
|
||||
|
||||
## 验证
|
||||
|
||||
侧栏无浏览器;推送页卡片/列表;点击名称复制。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 审计 — 各页搜索历史互不共享
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-12-search-history-isolated-scopes.md`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/utils/server_search_history.py` | terminal 上下文,上限 10 |
|
||||
| `server/api/servers.py` | terminal-search-history API |
|
||||
| `frontend/src/composables/useServerSearchHistory.ts` | terminal scope |
|
||||
| `frontend/src/composables/terminal/useTerminalSessions.ts` | 独立历史 |
|
||||
| `tests/test_server_search_history.py` | 隔离与 API 测试 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 安全 | PASS — admin 隔离 |
|
||||
| H2 | 无静默吞错 | PASS |
|
||||
| H3 | 无新密钥 | PASS |
|
||||
| H4 | CUD | N/A |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | admin_ui_preferences 分 context |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] type-check
|
||||
- [x] pytest test_server_search_history
|
||||
- [ ] 生产三页互不串历史验证
|
||||
|
||||
## 验证
|
||||
|
||||
servers / terminal / push 各搜一词,下拉互不可见。
|
||||
@@ -0,0 +1,46 @@
|
||||
# 审计 — 2026-06-12 新服务器接入自动化(生产部署)
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server/application/services/server_onboarding_service.py` | 调度入口 |
|
||||
| `server/application/services/server_batch_service.py` | `_run_onboard` |
|
||||
| `server/application/server_batch_common.py` | `detect_and_save_target_path` |
|
||||
| `server/api/servers.py` | 创建/导入/IP 添加钩子 |
|
||||
| `server/api/schemas.py` | `onboarding_job_id` |
|
||||
| `frontend/src/composables/servers/useServerFormDialog.ts` | 任务注册 |
|
||||
| `frontend/src/pages/ServersPage.vue` | IP 批量添加注册 |
|
||||
| `frontend/src/types/api.ts` | 类型 |
|
||||
| `frontend/src/utils/auditLabels.ts` | batch_server_onboard 标签 |
|
||||
| `scripts/batch_detect_target_path.py` | 复用 detect_and_save_target_path |
|
||||
| `tests/test_server_onboarding.py` | 单测 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | sudoers 白名单 | PASS |
|
||||
| H2 | 创建 API 不 5xx | PASS |
|
||||
| H3 | 探测失败可推 wwwroot | PASS |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | PASS | files_sudoers_service |
|
||||
| H2 | PASS | server_onboarding_service |
|
||||
| H3 | PASS | _run_onboard |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest test_server_onboarding
|
||||
- [x] changelog 2026-06-12-server-onboarding-automation.md
|
||||
- [x] 7/7 门控
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
pytest tests/test_server_onboarding.py -q
|
||||
bash deploy/pre_deploy_check.sh
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# 审计 — 服务器实时监测槽(M1+M2+M3,无 CSV)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-12-server-watch-slots.md`
|
||||
**设计**: `docs/design/specs/2026-06-12-server-watch-slots-design.md`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `server/domain/models/__init__.py` | WatchPinSession / WatchPin / WatchProbeRecord |
|
||||
| `server/infrastructure/database/migrations.py` | watch_* 表 + processes_json |
|
||||
| `server/infrastructure/database/watch_repo.py` | Pin CRUD、探针/会话查询 |
|
||||
| `server/application/services/watch_service.py` | 业务编排 |
|
||||
| `server/api/watch.py` | REST API |
|
||||
| `server/background/watch_probe_runner.py` | 5s 探针、进程、告警 |
|
||||
| `server/api/websocket.py` | `/ws/watch` |
|
||||
| `server/utils/watch_metrics.py` / `watch_alerts.py` | 指标与告警联动 |
|
||||
| `server/infrastructure/ssh/remote_probe.py` | SSH 全量探针 + 进程 |
|
||||
| `server/main.py` | 路由 + watch_probe 任务 |
|
||||
| `web/agent/agent.py` | 心跳 net_io/disk_io(M3 可选) |
|
||||
| `frontend/src/composables/useWatchPins.ts` | Pin + WS |
|
||||
| `frontend/src/components/watch/WatchSlotRow.vue` | 槽位行 |
|
||||
| `frontend/src/components/watch/WatchSlotCard.vue` | 单槽卡片 |
|
||||
| `frontend/src/components/watch/WatchSparkline.vue` | 迷你折线 |
|
||||
| `frontend/src/components/watch/WatchProcessDrawer.vue` | 进程 Top5 |
|
||||
| `frontend/src/components/watch/WatchTrendChart.vue` | 历史趋势 |
|
||||
| `frontend/src/components/watch/WatchProbeRecordsTable.vue` | 探针记录表 |
|
||||
| `frontend/src/utils/watchFormat.ts` | 速率格式化 |
|
||||
| `frontend/src/pages/WatchMetricsPage.vue` | 监测历史 |
|
||||
| `frontend/src/pages/ServersPage.vue` | 槽位行 + 列表 + |
|
||||
| `frontend/src/router/index.ts` / `App.vue` | 路由与导航 |
|
||||
| `server/api/servers.py` | 移除未使用 import |
|
||||
| `tests/test_watch_metrics.py` / `test_watch_pins.py` | 单测 |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 安全 | PASS — admin 隔离 Pin/探针记录;WS JWT |
|
||||
| H2 | 无静默吞错 | PASS — 探针失败 INSERT + error 字段 |
|
||||
| H3 | 无新密钥 | PASS |
|
||||
| H4 | CUD 审计 | PASS — watch_pin_add/remove/replace |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | Depends(get_current_admin);探针每 5s 落库 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] local_verify
|
||||
- [x] pytest test_watch_*
|
||||
- [x] frontend type-check
|
||||
- [ ] 生产浏览器:Pin 后 5s 指标更新、#/watch-metrics
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
bash scripts/local_verify.sh
|
||||
.venv/bin/pytest tests/test_watch_metrics.py tests/test_watch_pins.py -q
|
||||
cd frontend && npm run type-check
|
||||
```
|
||||
|
||||
未 Pin 子机无额外 SSH;满 4 槽替换对话框;Agent 升级可选(减 SSH,非强制)。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 审计 — 终端页右侧服务器列表与搜索历史
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-12-terminal-server-rail-search-history.md`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `frontend/src/pages/TerminalPage.vue` | 左右分栏,右侧常驻列表 |
|
||||
| `frontend/src/components/terminal/TerminalArea.vue` | 空态文案 |
|
||||
| `frontend/src/components/terminal/TerminalServerPicker.vue` | sidebar 模式 + combobox |
|
||||
| `frontend/src/components/terminal/TerminalServerPickerSearch.vue` | 新建 |
|
||||
| `frontend/src/components/terminal/TerminalServerPickerList.vue` | 新建 |
|
||||
| `frontend/src/composables/terminal/useTerminalSessions.ts` | search-history API |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 安全 | PASS — 复用已有 search-history API |
|
||||
| H2 | 无静默吞错 | PASS |
|
||||
| H3 | 无新密钥 | PASS |
|
||||
| H4 | CUD | N/A |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | PASS | 纯前端布局 + 已有 API |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] type-check
|
||||
- [ ] 生产 `#/terminal` 验证
|
||||
|
||||
## 验证
|
||||
|
||||
右侧列表 + 搜索历史 10 条;与会话并行。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 审计 — 监测槽 SSH 安装 psutil
|
||||
|
||||
**日期**:2026-06-12
|
||||
**Changelog**: `docs/changelog/2026-06-12-watch-install-psutil-ssh.md`
|
||||
|
||||
## 范围
|
||||
|
||||
| 文件 |
|
||||
|------|
|
||||
| `server/utils/psutil_install.py` |
|
||||
| `server/application/services/watch_service.py` |
|
||||
| `server/api/watch.py` |
|
||||
| `frontend/src/components/watch/WatchSlotCard.vue` |
|
||||
| `frontend/src/components/watch/WatchSlotRow.vue` |
|
||||
| `frontend/src/utils/watchFormat.ts` |
|
||||
| `server/utils/watch_probe_errors.py` |
|
||||
| `tests/test_psutil_install.py` |
|
||||
| `tests/test_watch_pins.py` |
|
||||
| `tests/test_watch_probe_errors.py` |
|
||||
|
||||
## Step 3
|
||||
|
||||
| H | 结论 |
|
||||
|---|------|
|
||||
| H1 鉴权 | PASS — 须 JWT;仅已 Pin 的服务器可安装 |
|
||||
| H2 吞错 | PASS — SSH/安装失败 422 + snackbar |
|
||||
| H4 审计 | PASS — `watch_install_psutil` |
|
||||
|
||||
## Closure
|
||||
|
||||
H1–H4 PASS
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest test_psutil_install + test_watch_pins
|
||||
- [x] npm run type-check
|
||||
- [ ] 生产浏览器:psutil 错误时出现按钮且安装后探针恢复
|
||||
@@ -0,0 +1,15 @@
|
||||
# 审计 — 监测槽 hover 外框高亮
|
||||
|
||||
**日期**:2026-06-12
|
||||
**Changelog**: `docs/changelog/2026-06-12-watch-slot-hover-border.md`
|
||||
|
||||
## 范围
|
||||
|
||||
| 文件 |
|
||||
|------|
|
||||
| `frontend/src/components/watch/WatchSlotCard.vue` |
|
||||
|
||||
## Step 3 / Closure / DoD
|
||||
|
||||
- PASS — 纯 CSS,无逻辑/安全风险
|
||||
- DoD: 浏览器悬停仅边框变色
|
||||
@@ -0,0 +1,67 @@
|
||||
# 审计 — 监测槽开关 / TTL / 到期竞态 / 探针中文 / 移除提示
|
||||
|
||||
**日期**:2026-06-12
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-11-watch-slot-monitoring-toggle.md`
|
||||
- `docs/changelog/2026-06-11-watch-probe-parse-error-fix.md`
|
||||
- `docs/changelog/2026-06-12-watch-ttl-expiry-race-fix.md`
|
||||
|
||||
## 范围(文件清单)
|
||||
|
||||
| 层级 | 文件 |
|
||||
|------|------|
|
||||
| 模型/迁移 | `server/domain/models/__init__.py` |
|
||||
| | `server/infrastructure/database/migrations.py` |
|
||||
| Repository | `server/infrastructure/database/watch_repo.py` |
|
||||
| Service | `server/application/services/watch_service.py` |
|
||||
| API | `server/api/watch.py` |
|
||||
| 探针 | `server/background/watch_probe_runner.py` |
|
||||
| | `server/infrastructure/ssh/remote_probe.py` |
|
||||
| 工具 | `server/utils/watch_metrics.py` |
|
||||
| | `server/utils/watch_state.py` |
|
||||
| | `server/utils/watch_probe_errors.py` |
|
||||
| 前端 | `frontend/src/api/index.ts` |
|
||||
| | `frontend/src/composables/useWatchPins.ts` |
|
||||
| | `frontend/src/components/watch/WatchSlotCard.vue` |
|
||||
| | `frontend/src/components/watch/WatchSlotRow.vue` |
|
||||
| | `frontend/src/components/watch/WatchProbeRecordsTable.vue` |
|
||||
| | `frontend/src/utils/watchFormat.ts` |
|
||||
| 测试 | `tests/test_watch_pins.py` |
|
||||
| | `tests/test_watch_metrics.py` |
|
||||
| | `tests/test_watch_probe_errors.py` |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 鉴权 / IDOR | PASS — PATCH/DELETE 均 `get_current_admin` + admin_id 隔离 |
|
||||
| H2 | 无静默吞错 | PASS — 探针失败落库;前端 remove/monitoring/ttl 均有 snackbar |
|
||||
| H3 | 无新密钥 | PASS |
|
||||
| H4 | CUD 审计 | PASS — pause/resume/ttl/remove |
|
||||
| H5 | 输入边界 | PASS — `Literal[8,24]`、slot 0–3 |
|
||||
| H6 | 迁移 | PASS — monitoring_enabled + ttl_hours ALTER |
|
||||
| H7 | TTL 竞态 | PASS — `_finalize_due_pins` 读/写路径同步到期 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H6 | PASS | 鉴权、审计、迁移、探针落库 |
|
||||
| H7 | PASS | 到期 ghost pin 不再显示空槽 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `pytest tests/test_watch_*.py` — 21 passed
|
||||
- [x] `npm run type-check`
|
||||
- [x] P0 TTL 竞态修复 + P2 onRemove snackbar
|
||||
- [ ] `bash deploy/pre_deploy_check.sh`
|
||||
- [ ] 生产浏览器终验
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
.venv/bin/pytest tests/test_watch_*.py -q
|
||||
cd frontend && npm run type-check
|
||||
bash scripts/local_verify.sh
|
||||
bash deploy/pre_deploy_check.sh
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
# 审计 — 文件管理「新建文件」修复
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-13-files-new-file-fix.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `frontend/src/composables/files/useFilesActions.ts` | `openNewFileDialog`、等待编辑器就绪 |
|
||||
| `frontend/src/composables/files/useFilesEditor.ts` | 导出 `waitForEditorWorkbench` |
|
||||
| `frontend/src/composables/files/useFilesPage.ts` | 透传 helper |
|
||||
| `frontend/src/components/files/FilesToolbar.vue` | 未选服务器时提示 |
|
||||
| `server/infrastructure/ssh/asyncssh_pool.py` | SFTP 写前确保父目录 |
|
||||
|
||||
## Step 3(规则扫描)
|
||||
|
||||
| 规则 | 结论 | 说明 |
|
||||
|------|------|------|
|
||||
| 鉴权 | PASS | 仍走 `/sync/write-file` + admin JWT |
|
||||
| 路径 | PASS | `validatePathSegment` 不变 |
|
||||
| 静默失败 | PASS | 编辑器未就绪有 warning snackbar |
|
||||
| SSH | PASS | `mkdir -p` 仅父目录 |
|
||||
|
||||
## Closure
|
||||
|
||||
| 文件 | 结论 |
|
||||
|------|------|
|
||||
| `useFilesActions.ts` | SAFE — snackbar 错误提示 |
|
||||
| `asyncssh_pool.py` | SAFE — 复用 `_ensure_remote_parent_dir` |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + 本审计
|
||||
- [x] frontend type-check
|
||||
- [ ] 生产验证新建文件
|
||||
@@ -0,0 +1,50 @@
|
||||
# 审计 — 快捷命令回车修复 + 离线告警 Telegram 分流
|
||||
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-13-terminal-quick-cmd-enter-fix.md`
|
||||
- `docs/changelog/2026-06-13-offline-alert-telegram-split.md`
|
||||
|
||||
**设计/计划**:
|
||||
- `docs/design/specs/2026-06-13-offline-alert-telegram-split-design.md`
|
||||
- `docs/design/plans/2026-06-13-offline-alert-telegram-split.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `frontend/src/composables/useTerminalQuickCommands.ts` | `formatQuickCmdForSend` | ☑ |
|
||||
| `frontend/src/composables/terminal/useTerminalSessions.ts` | `execQuickCmd` | ☑ |
|
||||
| `frontend/src/components/TerminalQuickCommandsSettings.vue` | 保存不再 append `\r` | ☑ |
|
||||
| `frontend/src/pages/SettingsPage.vue` | 离线专用 Bot UI | ☑ |
|
||||
| `frontend/src/types/api.ts` | 离线开关文案 | ☑ |
|
||||
| `frontend/src/utils/auditLabels.ts` | reveal 审计标签 | ☑ |
|
||||
| `server/infrastructure/telegram/__init__.py` | 离线路由 | ☑ |
|
||||
| `server/api/settings.py` | offline API + `_telegram_fetch_chats` | ☑ |
|
||||
| `server/config.py` | `TELEGRAM_OFFLINE_*` | ☑ |
|
||||
| `tests/test_telegram_offline_channel.py` | 新 | ☑ |
|
||||
| `tests/test_terminal_quick_commands.py` | 新 | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 离线 Token 敏感掩码 | SAFE — `SENSITIVE_KEYS` |
|
||||
| H2 | reveal 审计 | SAFE — `reveal_offline_telegram_token` |
|
||||
| H3 | 半配离线回退默认 Bot | SAFE — `resolve_offline_telegram_credentials` |
|
||||
| H4 | 资源告警不走离线 Bot | SAFE — `send_telegram_alert` 未改 |
|
||||
| H5 | 快捷命令 `\r` 与 `trim()` | SAFE — 发送时 `formatQuickCmdForSend` |
|
||||
| H6 | Layer3 不同步离线 Bot | SAFE — 设计 intentional |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H6 | SAFE | 代码 + pytest 10/10 + `test_ops_patrol_sync` 未破坏 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `pytest tests/test_telegram_offline_channel.py tests/test_terminal_quick_commands.py` passed
|
||||
- [x] `bash scripts/local_verify.sh` 26/26
|
||||
- [x] changelog / design / audit
|
||||
- [x] 无 SECRET_KEY/API_KEY/ENCRYPTION_KEY/DATABASE_URL 变更
|
||||
- [x] 前端 `vite build` 待部署脚本执行
|
||||
@@ -0,0 +1,28 @@
|
||||
# 审计 — SSH 监测探针引号修复
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-13-ssh-watch-probe-quote-fix.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `server/infrastructure/ssh/remote_probe.py` | `strip(chr(34))` | ☑ |
|
||||
| `tests/test_watch_metrics.py` | 探针脚本回归 | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 无新攻击面 | SAFE |
|
||||
| H2 | 恢复监测可用性 | SAFE — 热修复 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H2 | SAFE | 本地 bash 执行探针返回 status ok |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `test_ssh_watch_metrics_cmd_runs_locally` passed
|
||||
- [x] changelog / audit
|
||||
@@ -0,0 +1,33 @@
|
||||
# 审计 — Telegram 群聊 Chat ID 检测
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-13-telegram-chat-id-group-detect.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `server/api/settings.py` | `_extract_chats_from_telegram_updates`、群聊更新类型 | ☑ |
|
||||
| `frontend/src/pages/SettingsPage.vue` | 群聊检测说明与类型标签 | ☑ |
|
||||
| `tests/test_settings_telegram.py` | my_chat_member / callback 单测 | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | Token 仍经既有 settings 鉴权 | SAFE — 未改 auth |
|
||||
| H2 | getUpdates 仍先清 webhook | SAFE — `_telegram_clear_webhook_if_set` |
|
||||
| H3 | 不泄露其他用户 chat | SAFE — 仅返回去重 chat 元数据 |
|
||||
| H4 | 群聊隐私模式说明 | SAFE — UI 提示 `/start@Bot` |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | SAFE | `pytest tests/test_settings_telegram.py` 8/8 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `pytest tests/test_settings_telegram.py` passed
|
||||
- [x] changelog / audit
|
||||
- [x] 无密钥类 settings 变更
|
||||
- [x] 前端 build 由部署脚本执行
|
||||
@@ -0,0 +1,69 @@
|
||||
# 审计 — 监测历史北京时间 + 下线远程浏览器
|
||||
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-13-watch-history-beijing-time.md`
|
||||
- `docs/changelog/2026-06-12-remove-remote-browser.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `frontend/src/utils/datetime.ts` | `formatChartAxisBeijing` | ☑ |
|
||||
| `frontend/src/components/watch/WatchProbeRecordsTable.vue` | 北京时间列 | ☑ |
|
||||
| `frontend/src/components/watch/WatchSparkline.vue` | tooltip 北京时间 | ☑ |
|
||||
| `frontend/src/components/watch/WatchTrendChart.vue` | 轴/tooltip 北京时间 | ☑ |
|
||||
| `frontend/src/pages/WatchMetricsPage.vue` | 「北京时间」标签 | ☑ |
|
||||
| `frontend/src/App.vue` | 移除 GlobalBrowserPanel | ☑ |
|
||||
| `frontend/src/router/index.ts` | 移除 `/browser` 路由 | ☑ |
|
||||
| `frontend/src/pages/ServersPage.vue` | 站点 `window.open` | ☑ |
|
||||
| `frontend/src/pages/AuditPage.vue` | 移除未用 import | ☑ |
|
||||
| `frontend/src/components/GlobalBrowserPanel.vue` | 删除 | ☑ |
|
||||
| `frontend/src/composables/useGlobalBrowser.ts` | 删除 | ☑ |
|
||||
| `frontend/src/composables/useRemoteBrowserSession.ts` | 删除 | ☑ |
|
||||
| `frontend/src/pages/BrowserPage.vue` | 删除 | ☑ |
|
||||
| `server/main.py` | 注销 browser 路由 | ☑ |
|
||||
| `server/api/browser.py` | 删除 | ☑ |
|
||||
| `server/api/browser_session.py` | 删除 | ☑ |
|
||||
| `server/infrastructure/browser/` | 删除 | ☑ |
|
||||
| `__init__.py` | browser 包删除 | ☑ |
|
||||
| `server/utils/browser_ui_state.py` | 删除 | ☑ |
|
||||
| `server/utils/browser_url_safe.py` | 删除 | ☑ |
|
||||
| `browser-worker/` | 删除 | ☑ |
|
||||
| `Dockerfile` | browser-worker 删除 | ☑ |
|
||||
| `browser-worker/main.py` | 删除 | ☑ |
|
||||
| `browser-worker/browser_url_safe.py` | 删除 | ☑ |
|
||||
| `browser-worker/browser_stealth.py` | 删除 | ☑ |
|
||||
| `browser-worker/config.py` | 删除 | ☑ |
|
||||
| `browser-worker/docker-compose.yml` | 删除 | ☑ |
|
||||
| `browser-worker/docker-entrypoint.sh` | 删除 | ☑ |
|
||||
| `browser-worker/requirements.txt` | 删除 | ☑ |
|
||||
| `browser-worker/session_manager.py` | 删除 | ☑ |
|
||||
| `server/infrastructure/browser/__init__.py` | 删除 | ☑ |
|
||||
| `server/infrastructure/browser/session_registry.py` | 删除 | ☑ |
|
||||
| `server/infrastructure/browser/worker_client.py` | 删除 | ☑ |
|
||||
| `tests/test_browser_ui_state.py` | 删除 | ☑ |
|
||||
| `tests/test_browser_url_safe.py` | 删除 | ☑ |
|
||||
| `tests/chain/test_agent_dashboard_flow.py` | Redis pipeline mock | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 北京时间仅前端展示,不改 API 时区 | SAFE |
|
||||
| H2 | 浏览器下线后无孤立 `/api/browser` 注册 | SAFE |
|
||||
| H3 | `browserUrl.ts` 保留供站点外链 | SAFE |
|
||||
| H4 | 无新密钥/明文 | SAFE |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | SAFE | L2b 26/26;chain 9/9;vite build OK |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `bash scripts/local_verify.sh`
|
||||
- [x] `pytest tests/chain -q`
|
||||
- [x] `cd frontend && npx vite build`
|
||||
- [x] changelog + audit
|
||||
- [ ] 生产 `/health` + `#/watch-metrics` 北京时间终验
|
||||
@@ -0,0 +1,39 @@
|
||||
# 审计 — 监测插槽硬件详情
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-13-watch-slot-hardware-specs.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `server/utils/watch_metrics.py` | 硬件字段、fill_hardware_gaps | ☑ |
|
||||
| `server/infrastructure/ssh/remote_probe.py` | SSH 探针采集详情 | ☑ |
|
||||
| `server/background/watch_probe_runner.py` | 探针后补齐用量 | ☑ |
|
||||
| `web/agent/agent.py` | `_hardware_specs_dict` 心跳 | ☑ |
|
||||
| `frontend/src/components/watch/WatchSlotCard.vue` | 详情 UI | ☑ |
|
||||
| `frontend/src/components/watch/WatchMetricBar.vue` | sub 副行 | ☑ |
|
||||
| `frontend/src/utils/watchFormat.ts` | 格式化 | ☑ |
|
||||
| `frontend/src/composables/useWatchPins.ts` | 类型 | ☑ |
|
||||
| `tests/test_watch_metrics.py` | 单测 | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | cpu_model 长度截断 | SAFE — `_short_text` 120 字符 |
|
||||
| H2 | 无新公开端点 | SAFE — 复用 watch WS |
|
||||
| H3 | SSH 脚本无 shell 注入 | SAFE — 固定 python -c |
|
||||
| H4 | 旧 Agent 回退 | SAFE — `fill_hardware_gaps` 按 % 推算 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | SAFE | `pytest tests/test_watch_metrics.py` 13/13 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest watch_metrics passed
|
||||
- [x] changelog / audit
|
||||
- [x] 前端 build 由部署脚本执行
|
||||
- [x] 无密钥类配置变更
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user