Compare commits

20 Commits

Author SHA1 Message Date
r a16ba169fd fix(btpanel): 未装宝塔时一键登录返回友好提示
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
SSH/API 登录前检测面板;将 public 模块 Traceback 映射为可读中文;
bootstrap bt_not_installed 不再回退 SSH。AGENTS 约定改完推 Gitea。
2026-07-09 01:25:54 +08:00
r 9b583ccc91 docs(release): 主与 release 分支合并功能清单 + 导入 release 文档目录
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
便于 Gitea 上 main ↔ release/20260708-axs 合并评审与冲突对照。
2026-07-09 01:14:32 +08:00
r 5520434d53 build(frontend): 同步 web/app 构建产物与 ServersPage 调整
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
与 axs 全量重传前的本地工作区对齐。
2026-07-09 00:16:56 +08:00
r bd041136c0 chore(deploy): axs Gitea 为默认发布源 + 全量分批推送脚本
主项目推送到 https://axs.kuma1xn.vip 时分批提交避免 Cloudflare 524。
2026-07-08 23:58:52 +08:00
r 6859846b7c sync: 主项目全量同步(含 btpanel 一键登录回归修复与前端构建产物)
- bootstrap 失败恢复 SSH 临时登录回退
- ssh_bootstrap 真安装检测与 config 目录自愈
- axs Gitea 发布源切换为 https://axs.kuma1xn.vip
2026-07-08 23:52:40 +08:00
r d976cd03a3 feat(terminal): 工具栏增加宝塔一键登录按钮
终端有打开会话时,在字号左侧提供宝塔登录,复用 useBtPanelLogin。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 23:19:16 +08:00
Nexus Gate 41bff9dff6 补录 Gate 8 AI Review 缓存(65d8605b 批次) 2026-07-01 12:10:07 +08:00
Nexus Gate 65d8605b6c 补录 2026-07-01 部署门禁:Terminal 快速添加、快捷命令排序、Gate3 pytest 回退
6/30 生产部署跳过 pre_deploy_check;本提交补齐 changelog/audit 与门控修复。
2026-07-01 12:09:02 +08:00
r d50120d1d2 每日离线巡检发报前 SSH 复检离线子机并刷新心跳。
日报二次扫描反映复检后状态,Telegram 展示复检统计;离线列表与告警显示名优先备注。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 17:04:37 +08:00
r 0ec0aaf174 feat(btpanel): 一键登录临时 token 有效期改为 24 小时
API set_temp_login 传 expire_time=86400;SSH 回退用自定义脚本替代 tools.py 3 小时默认。
2026-06-22 05:48:15 +08:00
Nexus Agent 45d21026b7 feat(frontend): 跨服务器传输源机 A 展示可点击站点链接
与服务器列表同源 resolveServerSiteUrl;选 A 后显示域名链接。
2026-06-21 13:20:03 +08:00
Nexus Agent 71f0f53e6a feat(frontend): 跨服务器传输目标机 B 文件浏览
选 B 后展示目录列表,浏览时同步目标路径。
2026-06-21 13:15:01 +08:00
Nexus Agent 113718b553 fix(frontend): 跨服务器传输默认使用各机目标路径
选源机浏览 A.target_path;选目标机填入 B.target_path。
2026-06-21 13:09:48 +08:00
Nexus Agent da26d648a9 fix(frontend): 跨服务器传输页服务器选择与搜索
对齐文件页 v-autocomplete:v-model、客户端过滤、修正选项 slot。
2026-06-21 13:05:39 +08:00
Nexus Agent d770b07609 feat(frontend): 跨服务器传输独立页与侧栏菜单调整
将传输功能从文件页拆到 #/server-transfer,文件页改为跳转预填;监测历史移入系统分组。
2026-06-21 12:59:45 +08:00
r 73278667e0 feat(files): 跨服务器文件直传与打包投递
A→B SFTP 中继与一键 tar 打包 curl 拉取;含审计修复与 18 项单测。
2026-06-21 12:48:50 +08:00
r 76682e3be0 ui(nav): 侧栏隐藏宝塔面板菜单分组
宝塔能力保留在 servers 页一键登录与批量获取 API;btpanel 路由仍可通过直链访问。
2026-06-21 12:22:14 +08:00
r 629d8b3e26 fix(btpanel): login-url 返回类型含 bool 导致全站 500
路由注解 dict[str,str] 与 bootstrapped 布尔字段冲突,FastAPI 响应校验失败。
2026-06-21 12:17:29 +08:00
r 37720c4705 feat(btpanel): 一键登录自动获取 API 与倒计时提示
未配置机器点击登录时后端 immediate bootstrap;前端顶部 snackbar 显示约 20 秒倒计时。
2026-06-21 12:09:34 +08:00
r 4f260f9257 fix(servers): 批量栏已选区增加「选择」标题对齐
与运维/Agent/其他分组同结构底对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:41:30 +08:00
4294 changed files with 76030 additions and 1044 deletions
+6 -1
View File
@@ -38,11 +38,16 @@ FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA15 页)· Vite
## 部署
默认 **本机 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/`
## 进度条(改代码时输出)
+10 -6
View File
@@ -19,13 +19,12 @@ cd frontend && npm run dev # :3000/app/
路径见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)。
## Git本机快照
## Git(本机 + Gitea
- 本地 `git commit` 用于回滚;**不 `git push`**(除非你明确要求)
- 已移除 `origin` 跟踪,避免误推;原远程:`http://66.154.115.8:3000/admin/Nexus.git`(需恢复时说一声)。
- `.env``SECRETS.md``*.pem` 已在 `.gitignore`**勿提交**。
- 改完代码:**`git commit` `bash scripts/git-push.sh` 推到 Gitea**(凭据 `deploy/nexus-1panel.secrets.sh`);大仓库可用 `scripts/git-push-axs-full.sh`
- 本地 commit 仍用于回滚;**不要漏推 Gitea**(用户约定 2026-07-09)。
- `.env``SECRETS.md``*.pem``deploy/nexus-1panel.secrets.sh` 已在 `.gitignore`**勿提交**。
- 改代码仍写 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)。
- 用户说「提交快照 / commit」时执行。
## 强制约束
@@ -40,11 +39,16 @@ cd frontend && npm run dev # :3000/app/
## 部署
默认 **本机 rsync → SSH 生产机****代码先 push Gitea 再部署**(见上节 Git):
```bash
bash deploy/pre_deploy_check.sh
bash deploy/deploy-production.sh # 需 SSH nexus + 用户批准
bash scripts/git-push.sh # 或 git-push-axs-full.sh
bash deploy/deploy-production.sh # rsync + Docker upgrade --skip-git + 前端
```
可选:`NEXUS_DEPLOY_VIA_GIT=1` 恢复远程 `git pull`
详情见功能指南 §1718。
## 归档文档
+58 -24
View File
@@ -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,37 +104,61 @@ 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..."
cd frontend && npx vite build && cd ..
tar czf /tmp/nexus-frontend.tar.gz -C web/app index.html assets/
scp_cmd /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
ssh_cmd "cd ${DEPLOY_PATH}/web/app && tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz"
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py --dry-run"
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"
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 "ERROR: 无法识别远程运行时(非 Docker 也非 Supervisor" >&2
echo " 请手动: ssh ${NEXUS_SSH_HOST} 'sudo nx update'" >&2
exit 1
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" ]]; then
# Host web/app must match image build before sync_webapp_to_container (else stale assets overwrite container).
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 && sudo 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"
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app --dry-run"
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
fi
fi
if [[ "$RUNTIME" == "docker" ]]; then
echo "▶ Sync host web/app 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
+604
View File
@@ -2378,3 +2378,607 @@
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"summary","result":"BLOCK","detail":"6/8"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"changelog","result":"PASS","detail":"2026-06-18-terminal-persist-navigation.md 40lines"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"shell_eol","result":"BLOCK","detail":"CR in tracked sh"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"summary","result":"BLOCK","detail":"5/8"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"security","result":"BLOCK","detail":"2 HIGH findings"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"security","result":"BLOCK","detail":"2 HIGH findings"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"shell_eol","result":"BLOCK","detail":"CR in tracked sh"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"2/8"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md 29lines"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md 29lines"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md 29lines"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"6/8"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-deploy.md 30lines"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-ssh-bootstrap.md"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-deploy.md 32lines"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-ssh-bootstrap.md"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md 33lines"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"6/8"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md 33lines"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md 38lines"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md 38lines"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-nav-hidden.md 23lines"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-nav-hidden.md"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-file-transfer-audit-fixes.md 35lines"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-file-transfer.md"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-file-transfer-audit-fixes.md 35lines"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-file-transfer.md"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-nav-watch-metrics-under-system.md 23lines"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-page.md"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-server-picker-fix.md 24lines"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-page.md"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-target-path-default.md 28lines"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-page.md"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-dest-browser.md 20lines"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-page.md"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-source-site-link.md 17lines"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"audit","result":"BLOCK","detail":"missing sections"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-source-site-link.md 17lines"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"audit","result":"BLOCK","detail":"missing sections"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-source-site-link.md 17lines"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-deploy-local-rsync.md 39lines"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-deploy-local-rsync.md 47lines"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-deploy-local-rsync.md 48lines"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-manual-dest.md 31lines"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-dest-write-elevation.md 31lines"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-curl-tmp-staging.md 29lines"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-btpanel-download-tar-overwrite.md 39lines"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-btpanel-download-tar-overwrite.md 43lines"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"summary","result":"BLOCK","detail":"5/8"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"changelog","result":"PASS","detail":"2026-06-22-btpanel-temp-login-24h.md 30lines"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"audit","result":"PASS","detail":"2026-06-22-btpanel-temp-login-24h.md"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-subscription-ip-refresh-primary-fix.md 36lines"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"summary","result":"BLOCK","detail":"4/8"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md 43lines"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"summary","result":"BLOCK","detail":"4/8"}
{"ts":"2026-06-23T21:06:32+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md 43lines"}
{"ts":"2026-06-23T21:06:32+08:00","date":"2026-06-23","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-23T21:06:32+08:00","date":"2026-06-23","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md 43lines"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"audit","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md 43lines"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"audit","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md 42lines"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"summary","result":"BLOCK","detail":"5/8"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md 42lines"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md 42lines"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-servers-unset-path-table-align.md 77lines"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-servers-unset-path-table-align.md 77lines"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-servers-unset-path-table-align.md 77lines"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"summary","result":"BLOCK","detail":"3/8"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"summary","result":"BLOCK","detail":"3/8"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"changelog","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md 54lines"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"audit","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"changelog","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md 54lines"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"audit","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"changelog","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md 54lines"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"audit","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"changelog","result":"PASS","detail":"2026-07-01-agent-watch-dual-mode.md 38lines"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"audit","result":"PASS","detail":"2026-07-01-agent-watch-dual-mode.md"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-agent-cpu-sampling.md 35lines"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-agent-cpu-sampling.md"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"changelog","result":"PASS","detail":"2026-07-03-terminal-btpanel-login-button.md 29lines"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"audit","result":"PASS","detail":"2026-07-03-terminal-btpanel-login-button.md"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"ai_review","result":"PASS","detail":"ok"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"summary","result":"PASS","detail":"8/8"}
+1
View File
@@ -2,6 +2,7 @@
# 用于 git pushscripts/git-push.sh);clone/pull 仍可匿名
# NEXUS_GITEA_HOST='66.154.115.8:3000'
# NEXUS_GITEA_HOST='axs.kuma1xn.vip' # HTTPS Gitea2026-07-08 起,分支 release/20260708-axs
# NEXUS_GITEA_REPO='admin/Nexus.git'
# admin 账号密码或 Access Token
# NEXUS_GITEA_TOKEN='你的密码或令牌'
+22 -5
View File
@@ -23,7 +23,7 @@ set -euo pipefail
# 内置路径/域名(无敏感信息,可提交 Git)
# ═══════════════════════════════════════════════════════════════════
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
@@ -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}"
@@ -725,10 +726,14 @@ require_token() {
}
git_remote_url() {
local scheme=http
if [[ "$NEXUS_GITEA_HOST" != *:* ]]; then
scheme="${NEXUS_GITEA_SCHEME:-https}"
fi
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
echo "${scheme}://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
else
echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
echo "${scheme}://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
fi
}
@@ -1138,8 +1143,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 +1158,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 +1175,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"
@@ -1272,6 +1288,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 ;;
+32 -5
View File
@@ -131,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"
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Sync local Nexus working tree to production host (no Gitea push/pull).
# Transfer via tar|ssh; remote ownership auto-detected (www:www on BT, else deploy path owner).
#
# Usage:
# bash deploy/rsync-local-to-server.sh
# NEXUS_DEPLOY_PATH=/opt/nexus bash deploy/rsync-local-to-server.sh
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT}"
SECRETS="${ROOT}/deploy/nexus-1panel.secrets.sh"
if [[ -f "$SECRETS" ]]; then
# shellcheck disable=SC1090
source "$SECRETS"
fi
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
fi
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
exit 1
fi
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-}"
if [[ -z "$DEPLOY_PATH" ]]; then
DEPLOY_PATH="$(ssh_cmd 'for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do [[ -d "$d/server" ]] && echo "$d" && exit 0; done; echo /opt/nexus')"
fi
resolve_deploy_chown() {
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
echo "${NEXUS_DEPLOY_CHOWN}"
return
fi
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
echo "www:www"
return
fi
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
}
DEPLOY_CHOWN="$(resolve_deploy_chown)"
echo "═══ Nexus local → server sync ═══"
echo "SSH target: ${NEXUS_SSH_HOST}"
echo "Remote path: ${DEPLOY_PATH}"
echo "Owner: ${DEPLOY_CHOWN}"
TAR_EXCLUDES=(
--exclude='.git'
--exclude='.env'
--exclude='.env.*'
--exclude='SECRETS.md'
--exclude='deploy/nexus-1panel.secrets.sh'
--exclude='docker/.env.prod'
--exclude='web/data'
--exclude='frontend/node_modules'
--exclude='node_modules'
--exclude='.venv'
--exclude='venv'
--exclude='__pycache__'
--exclude='.cursor'
--exclude='.install_locked'
--exclude='.install_state.json'
--exclude='web/app/assets'
--exclude='tmp'
--exclude='frontend/test-results'
--exclude='frontend/e2e/.auth'
)
echo "▶ tar stream → ${DEPLOY_PATH} ..."
tar -C "${ROOT}" -czf - "${TAR_EXCLUDES[@]}" . | \
ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "sudo tar -xzf - -C '${DEPLOY_PATH}'"
echo "▶ chown ${DEPLOY_CHOWN} (skip docker/.env.prod, web/data, .env) ..."
ssh_cmd "sudo bash -s" <<REMOTE
set -euo pipefail
root='${DEPLOY_PATH}'
own='${DEPLOY_CHOWN}'
chown_tree() {
local p="\$1"
[[ -e "\$p" ]] || return 0
sudo chown -R "\$own" "\$p"
}
for top in server deploy scripts tests standards docs frontend web; do
chown_tree "\$root/\$top"
done
if [[ -d "\$root/docker" ]]; then
while IFS= read -r -d '' p; do
sudo chown -R "\$own" "\$p"
done < <(find "\$root/docker" \( -path "\$root/docker/.env.prod" \) -prune -o -print0)
fi
for f in pyproject.toml requirements.txt requirements-dev.txt AGENTS.md CLAUDE.md; do
[[ -f "\$root/\$f" ]] && sudo chown "\$own" "\$root/\$f"
done
REMOTE
echo "✓ sync complete (owner ${DEPLOY_CHOWN})"
@@ -0,0 +1,48 @@
# 审计 — 2026-06-21 宝塔一键登录自动 bootstrap
**Changelog**: `docs/changelog/2026-06-21-btpanel-login-auto-bootstrap.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `btpanel_service.py` `create_login_url` | 未配置先 bootstrap,失败显式 ValueError;审计含 bootstrapped | PASS |
| `useBtPanelLogin.ts` | 无静默吞错,成功提示区分 bootstrap | PASS |
| `BtPanelLoginPage.vue` | 移除未配置拦截,与 servers 页一致 | PASS |
| `bootstrap_server` | 复用既有锁与 immediate 源,无重复 SSH 逻辑 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 未配置仅 SSH 登录 | FIXED | login-url 先 immediate bootstrap |
| 前端需手动获取 API | FIXED | 一键登录单请求完成 |
| 宝塔页拦截未配置 | FIXED | 与 servers 行为统一 |
## 文件清单
- `server/application/services/btpanel_service.py`
- `server/infrastructure/btpanel/client.py`
- `frontend/src/App.vue`
- `frontend/src/api/btpanel.ts`
- `frontend/src/types/global.d.ts`
- `frontend/src/composables/btpanel/useBtPanelLogin.ts`
- `frontend/src/composables/btpanel/useBtPanelBatchBootstrap.ts`
- `frontend/src/components/btpanel/BtPanelBatchBootstrapDialog.vue`
- `frontend/src/components/servers/ServerBatchActionBar.vue`
- `frontend/src/components/servers/ServerUnsetPathPanel.vue`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue`
- `frontend/src/pages/btpanel/BtPanelMonitorPage.vue`
- `tests/test_btpanel_login_url.py`
- `tests/test_btpanel_client.py`
- `tests/test_btpanel_login_url_route.py`
- `server/api/btpanel.py`
- `docs/changelog/2026-06-21-btpanel-login-auto-bootstrap.md`
## DoD
- [x] `#/servers` 一键登录未配置时自动获取 API 后打开面板
- [x] bootstrap 失败返回可读错误
- [x] `pytest tests/test_btpanel_login_url.py` 通过
- [x] changelog 已写
@@ -0,0 +1,26 @@
# 审计 — 2026-06-21 侧栏隐藏宝塔菜单
**Changelog**: `docs/changelog/2026-06-21-btpanel-nav-hidden.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `App.vue` | 仅移除导航 UI,路由与 API 未删 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 侧栏宝塔入口 | DONE | 整组菜单隐藏 |
| servers 一键登录 | OK | 未改动 |
## 文件清单
- `frontend/src/App.vue`
- `docs/changelog/2026-06-21-btpanel-nav-hidden.md`
## DoD
- [x] 侧栏无「宝塔面板」分组
- [x] changelog 已写
@@ -0,0 +1,30 @@
# 审计 — 2026-06-21 宝塔临时登录 24 小时
**Changelog**: `docs/changelog/2026-06-21-btpanel-temp-login-24h.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `constants.py` | 单点 TTL 常量 | PASS |
| `btpanel_service.py` | API 传 expire_timetoken 拼 URL | PASS |
| `ssh_login.py` | base64 远程脚本,无静默吞错 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 默认 3h 临时登录 | FIXED | API + SSH 均 86400s |
## 文件清单
- `server/infrastructure/btpanel/constants.py`
- `server/application/services/btpanel_service.py`
- `server/infrastructure/btpanel/ssh_login.py`
- `tests/test_btpanel_temp_login_ttl.py`
- `docs/changelog/2026-06-21-btpanel-temp-login-24h.md`
## DoD
- [x] expire_time = now + 86400
- [x] pytest 通过
@@ -0,0 +1,46 @@
# 审计 — 2026-06-21 跨服务器文件传输
**Changelog**: `docs/changelog/2026-06-21-server-file-transfer.md``docs/changelog/2026-06-21-server-file-transfer-audit-fixes.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `server_file_transfer_service.py` | 路径 `shlex.quote`、直传 500MB 上限、deliver 失败回滚 | PASS |
| `transfer-download` | token Redis TTLJWT 前缀白名单 | PASS(已知:前缀对所有 method 生效) |
| `useFilesActions.ts` | 模式推荐基于 `transferItems`relay/deliver 分路径缓存 | PASS |
| `test_server_file_transfer.py` | deliver 成功/回滚/超限、relay、store | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| deliver 失败泄漏归档/token | FIXED | rollback cleanup |
| deliver 成功 token 残留 | FIXED | delete_package_meta |
| 右键传输模式误判 | FIXED | transferItems |
| 直传多项中途失败无回滚 | ACCEPT | v1 文档化 |
## 文件清单
- `server/application/services/server_file_transfer_service.py`
- `server/infrastructure/redis/transfer_package_store.py`
- `server/api/sync_v2.py`
- `server/api/schemas.py`
- `server/api/auth_jwt.py`
- `frontend/src/composables/files/useFilesActions.ts`
- `frontend/src/composables/files/useFilesPage.ts`
- `frontend/src/components/files/FilesDialogs.vue`
- `frontend/src/components/files/FilesList.vue`
- `frontend/src/components/files/FilesToolbar.vue`
- `frontend/src/utils/auditLabels.ts`
- `tests/test_server_file_transfer.py`
- `docs/design/specs/2026-06-21-server-file-transfer-design.md`
- `docs/design/plans/2026-06-21-server-file-transfer.md`
- `docs/changelog/2026-06-21-server-file-transfer.md`
- `docs/changelog/2026-06-21-server-file-transfer-audit-fixes.md`
## DoD
- [x] 直传 + 打包投递 API 与前端对话框
- [x] `pytest tests/test_server_file_transfer.py` 18 passed
- [x] changelog ≥10 行
@@ -0,0 +1,41 @@
# 审计 — 2026-06-21 跨服务器传输独立页 + 侧栏调整
**Changelog**: `docs/changelog/2026-06-21-server-transfer-page.md``docs/changelog/2026-06-21-nav-watch-metrics-under-system.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `useServerFileTransfer.ts` | 路径 normalize、relay 500MB 禁用、localStorage 分模式缓存 | PASS |
| `useFilesActions.ts` | 移除内嵌传输;跳转 query 预填 | PASS |
| `ServerTransferPage.vue` | 顶层解构 ref,模板类型正确 | PASS |
| `App.vue` | 仅菜单分组调整,路由不变 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 文件页内嵌传输对话框 | REMOVED | 改独立页 |
| 监测历史菜单位置 | MOVED | 运维 → 系统 |
## 文件清单
- `frontend/src/pages/ServerTransferPage.vue`
- `frontend/src/composables/useServerFileTransfer.ts`
- `frontend/src/router/index.ts`
- `frontend/src/App.vue`
- `frontend/src/composables/files/useFilesActions.ts`
- `frontend/src/composables/files/useFilesPage.ts`
- `frontend/src/components/files/FilesDialogs.vue`
- `frontend/src/components/files/FilesToolbar.vue`
- `docs/project/nexus-functional-development-guide.md`
- `docs/changelog/2026-06-21-server-transfer-page.md`
- `docs/changelog/2026-06-21-nav-watch-metrics-under-system.md`
## DoD
- [x] 独立页 `#/server-transfer` + 侧栏入口
- [x] 文件页跳转预填 query
- [x] 监测历史归入系统分组
- [x] `npx vite build` 通过
- [x] changelog ≥10 行
@@ -0,0 +1,32 @@
# 审计 — 2026-06-21 跨服务器传输 UI 补强
**Changelog**: `docs/changelog/2026-06-21-server-transfer-dest-browser.md``docs/changelog/2026-06-21-server-transfer-source-site-link.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `useServerFileTransfer.ts` | B 机 browse 同步 destPath;站点 URL 计算 | PASS |
| `ServerTransferPage.vue` | 双端文件表;A 机站点外链 `rel=noopener` | PASS |
| `useServerList.ts` | ServerBrief 扩展 extra_attrs 供站点解析 | PASS |
## 文件清单
- `frontend/src/composables/useServerFileTransfer.ts`
- `frontend/src/composables/useServerList.ts`
- `frontend/src/pages/ServerTransferPage.vue`
- `docs/changelog/2026-06-21-server-transfer-dest-browser.md`
- `docs/changelog/2026-06-21-server-transfer-source-site-link.md`
## Closure
| 项 | 状态 |
|----|------|
| B 机目录浏览 | DONE |
| A 机站点链接 | DONE |
## DoD
- [x] B 机文件列表与路径同步
- [x] A 机下方可点击站点 URL
- [x] `vite build` 通过
@@ -0,0 +1,30 @@
# 审计 — 2026-06-22 宝塔临时登录 24 小时
**Changelog**: `docs/changelog/2026-06-22-btpanel-temp-login-24h.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `constants.py` | 单点 TTL 常量 | PASS |
| `btpanel_service.py` | API 传 expire_timetoken 拼 URL | PASS |
| `ssh_login.py` | base64 远程脚本,无静默吞错 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 默认 3h 临时登录 | FIXED | API + SSH 均 86400s |
## 文件清单
- `server/infrastructure/btpanel/constants.py`
- `server/application/services/btpanel_service.py`
- `server/infrastructure/btpanel/ssh_login.py`
- `tests/test_btpanel_temp_login_ttl.py`
- `docs/changelog/2026-06-22-btpanel-temp-login-24h.md`
## DoD
- [x] expire_time = now + 86400
- [x] pytest 通过
@@ -0,0 +1,46 @@
# 审计 — 检测路径改为 nginx 网站目录
## 范围(文件清单)
| 文件 | 变更 |
|------|------|
| `server/utils/nginx_path_detect.py` | 新增:构建远程 nginx root 检测命令 |
| `server/application/server_batch_common.py` | `detect_and_save_target_path` 改用 nginx |
| `frontend/src/pages/ServersPage.vue` | 确认对话框文案 |
| `scripts/batch_detect_target_path.py` | 脚本说明 |
| `tests/test_nginx_path_detect.py` | 单元测试 |
| `tests/test_server_onboarding.py` | mock 错误文案 |
| `server/application/services/btpanel_service.py` | 同批部署:宝塔临时登录 TTL |
| `server/infrastructure/btpanel/constants.py` | 同上 |
| `server/infrastructure/btpanel/ssh_login.py` | 同上 |
| `tests/test_btpanel_temp_login_ttl.py` | 同上 |
## Step 3 规则扫描
| H | 规则 | 结论 |
|---|------|------|
| H1 | 远程命令 domain 须 `shlex.quote` | PASS |
| H2 | 解析路径走 `normalize_remote_abs_path` | PASS |
| H3 | 非 root SSH 仍 `sudo_wrap` | PASS |
| H4 | 未静默吞 SSH 错误 | PASS — 无 stdout 返回明确错误 |
## Closure
| H | 判定 | 依据 |
|---|------|------|
| H1 | SAFE | `nginx_path_detect.py` `shlex.quote(host)` |
| H2 | SAFE | `server_batch_common.py` normalize 后写入 |
| H3 | SAFE | 沿用既有 `sudo_wrap` |
| H4 | SAFE | `未找到 nginx 网站目录` |
## DoD
- [x] pytest tests/test_nginx_path_detect.py
- [x] changelog 2026-06-23-detect-path-nginx-root.md
- [x] 功能指南 § 检测目标路径 已更新
## 验证
```bash
pytest tests/test_nginx_path_detect.py tests/test_server_onboarding.py -q
```
@@ -0,0 +1,53 @@
# 审计 — IP-only 服务器自动补全站点域名
## 范围(文件清单)
| 文件 | 变更 |
|------|------|
| `server/utils/site_host.py` | 新增:IP 判定与站点 host 解析 |
| `server/utils/nginx_domain_detect.py` | 新增:远程 nginx 域名探测命令 |
| `server/application/server_batch_common.py` | `detect_and_save_site_url` / `update_server_site_url` |
| `server/application/services/ip_domain_detect_schedule.py` | 新增:23:30 定时筛选与触发 |
| `server/background/ip_domain_detect_loop.py` | 新增:background loop |
| `server/application/services/server_batch_service.py` | batch op `detect-domain` |
| `server/config.py` | `IP_DOMAIN_DETECT_*` 配置 |
| `server/main.py` | 注册 loop |
| `tests/test_site_host.py` | 单元测试 |
| `tests/test_nginx_domain_detect.py` | 单元测试 |
| `tests/test_ip_domain_detect_schedule.py` | 单元测试 |
| `server/application/services/btpanel_service.py` | 同批未提交:宝塔临时登录 TTL |
| `server/infrastructure/btpanel/constants.py` | 同上 |
| `server/infrastructure/btpanel/ssh_login.py` | 同上 |
| `tests/test_btpanel_temp_login_ttl.py` | 同上 |
## Step 3 规则扫描
| H | 规则 | 结论 |
|---|------|------|
| H1 | 远程 shell 参数须 `shlex.quote` | PASS — `target_hint` 已 quote |
| H2 | 不改 SSH `domain` 字段 | PASS — 仅写 `extra_attrs.site_url` |
| H3 | 非 root SSH 仍 `sudo_wrap` | PASS — 沿用 detect-path 模式 |
| H4 | 未静默吞 SSH 错误 | PASS — 无 stdout 返回明确错误 |
| H5 | 每日 Redis 去重防重复 batch | PASS — `already_ran_today` |
## Closure
| H | 判定 | 依据 |
|---|------|------|
| H1 | SAFE | `nginx_domain_detect.py` `shlex.quote(hint)` |
| H2 | SAFE | `update_server_site_url` 只 merge extra_attrs |
| H3 | SAFE | `detect_and_save_site_url` 调用 `sudo_wrap` |
| H4 | SAFE | `未找到 nginx 站点域名` |
| H5 | SAFE | `ip_domain_detect_schedule.py` Redis key |
## DoD
- [x] pytest tests/test_site_host.py tests/test_nginx_domain_detect.py tests/test_ip_domain_detect_schedule.py
- [x] changelog 2026-06-24-ip-domain-detect-schedule.md
- [x] 设计文档 specs + plans
## 验证
```bash
.venv/bin/pytest tests/test_site_host.py tests/test_nginx_domain_detect.py tests/test_ip_domain_detect_schedule.py -q
```
@@ -0,0 +1,31 @@
# 审计 — 2026-06-29 宝塔 bootstrap 跳过无意义 reload
**Changelog**: `docs/changelog/2026-06-29-btpanel-bootstrap-skip-reload.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `ssh_bootstrap.py` | 有变更才 reload;仍原子写 api.json | PASS |
| `test_btpanel_ssh_bootstrap.py` | 远程脚本含 `if actions:` 先于 reload | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 重复 bootstrap 踢面板会话 | FIXED | `actions` 空则跳过写盘与 reload |
| 首次对接须 reload | OK | 新 token / 开 API / 加白名单仍 reload |
## 文件清单
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `tests/test_btpanel_ssh_bootstrap.py`
- `docs/design/specs/2026-06-20-btpanel-module-design.md`
## DoD
- [x] Step 3 规则扫描
- [x] Closure 表
- [x] 文件清单与本次改动对齐
- [x] `pytest tests/test_btpanel_ssh_bootstrap.py` 16 passed
- [x] 生产 rsync + Docker upgrade
@@ -0,0 +1,44 @@
# 审计 — 每日离线巡检发报前 SSH 复检
## 范围(文件清单)
| 文件 | 变更 |
|------|------|
| `server/application/server_connectivity.py` | `health_check_server_ids`;扫描列表 `batch_server_display_name` |
| `server/application/services/offline_daily_report_schedule.py` | preflight 后二次 snapshot |
| `server/config.py` | `OFFLINE_DAILY_REPORT_HEALTH_CHECK` |
| `server/infrastructure/telegram/__init__.py` | `preflight` 参数与文案 |
| `server/background/server_offline_monitor.py` | 告警显示名备注优先 |
| `tests/test_offline_daily_report_schedule.py` | preflight 单测 |
## Step 3 规则扫描
| H | 规则 | 结论 |
|---|------|------|
| H1 | 复用既有 SSH 探测,不新造协议 | PASS — `ssh_health_probe` + `write_ssh_health_heartbeat` |
| H2 | 并发受控 | PASS — `Semaphore(10)` 与批量健康检查一致 |
| H3 | 可配置关闭 | PASS — `OFFLINE_DAILY_REPORT_HEALTH_CHECK` |
| H4 | 日报数据为复检后 snapshot | PASS — 二次 `collect_offline_snapshot` |
| H5 | 显示名与批量任务 SSOT 一致 | PASS — `batch_server_display_name` |
## Closure
| H | 判定 | 依据 |
|---|------|------|
| H1 | SAFE | `server_connectivity.health_check_server_ids` |
| H2 | SAFE | `asyncio.Semaphore(max(1, concurrency))` |
| H3 | SAFE | `_health_check_enabled()` |
| H4 | SAFE | `offline_daily_report_schedule.run_scheduled_offline_daily_report` |
| H5 | SAFE | `scan_monitored_connectivity` / `server_offline_monitor` |
## DoD
- [x] pytest tests/test_offline_daily_report_schedule.py tests/test_batch_server_display_name.py
- [x] changelog 2026-06-30-offline-daily-preflight-health-check.md
- [x] 无 DB 迁移
## 验证
```bash
.venv/bin/pytest tests/test_offline_daily_report_schedule.py tests/test_batch_server_display_name.py -q
```
@@ -0,0 +1,41 @@
# 审计 — Agent 双模式四槽监测
**Changelog**: `docs/changelog/2026-07-01-agent-watch-dual-mode.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `agent.py` (子机) | watch 模式每 5s 全量;退出 watch 回 60s | PASS |
| `agent.py` (中心) | `watch_active` 仅查 monitoring pin 计数 | PASS |
| `watch_probe_runner` | 仅 `watch_mode`+新鲜心跳跳过 SSH | PASS |
| 进程列表 | 仍 SSH 偶发拉取 | PASS(已知限制) |
## Closure
| 项 | 状态 |
|----|------|
| 四槽长期 SSH 风控 | 有 Agent 时改走出站 5s |
| 双 Agent 并存 | 不需要,单进程双模式 |
| 旧 Agent 兼容 | SSH 兜底 |
## 文件清单
- `web/agent/agent.py`
- `web/agent/config.example.json`
- `server/api/agent.py`
- `server/background/watch_probe_runner.py`
- `server/utils/watch_metrics.py`
- `tests/test_agent_watch_mode.py`
## DoD
- [x] pytest 19 passedwatch + agent_watch_mode
- [x] 心跳响应含 watch_active
- [x] Agent 2.1.0 watch_mode 字段
## 验证
```bash
.venv/bin/pytest tests/test_agent_watch_mode.py tests/test_watch_metrics.py -q
```
@@ -0,0 +1,53 @@
# 审计 — 2026-07-01 补录门禁(宝塔 / Terminal / 快捷命令)
**Changelog**: `docs/changelog/2026-07-01-gate-catchup-terminal-btpanel.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `ssh_bootstrap.py` | 无变更不写盘、不 reload | PASS |
| `terminal.py` | 静态路由 `reorder` 先于 `{id}` | PASS |
| `useServerQuickAdd.ts` | 复用 Servers `add-by-ip` 契约 | PASS |
| `ServerQuickAddDialogs.vue` | 与 Servers 弹窗一致 | PASS |
| `TerminalPage.vue` | 成功后 `loadServers` + `newSession` | PASS |
| `TerminalQuickCommandsSettings.vue` | `@click.stop` 防列表吞事件 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 重复 bootstrap 踢宝塔会话 | FIXED | `if actions:` 才 reload |
| reorder 被 `{id}` 抢路由 | FIXED | 路由顺序调整 + 单测 |
| Terminal 无快速添加 | FIXED | TabBar 按钮 + 共用对话框 |
| 6/30 部署跳过门禁 | 补录 | 本 changelog/audit + pre_deploy |
## 文件清单
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `server/api/terminal.py`
- `tests/test_btpanel_ssh_bootstrap.py`
- `tests/test_terminal_quick_commands.py`
- `frontend/src/composables/servers/useServerQuickAdd.ts`
- `frontend/src/components/servers/ServerQuickAddDialogs.vue`
- `frontend/src/utils/openServerTerminal.ts`
- `frontend/src/components/terminal/TerminalTabBar.vue`
- `frontend/src/pages/TerminalPage.vue`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/components/TerminalQuickCommandsSettings.vue`
- `deploy/pre_deploy_check.sh`
## DoD
- [x] Step 3 规则扫描
- [x] Closure 表
- [x] 文件清单与本次批次一致
- [x] `pytest tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py` 通过
- [x] `pre_deploy_check.sh` 全门通过(Gate 3 回退或本机 :8600
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py -q
bash deploy/pre_deploy_check.sh
```
@@ -0,0 +1,39 @@
# 审计 — Agent 2.1.1 CPU 采样
**Changelog**: `docs/changelog/2026-07-02-agent-cpu-sampling.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `cpu_metrics.py` | watch 首次 1s bootstrap,后续 interval=None 对齐 ~5s 心跳 | PASS |
| `agent.py` | 进入 watch 时 `reset_watch_cpu_bootstrap()` | PASS |
| `server_batch_common` | 升级命令含 cpu_metrics.py | PASS |
| 常态 60s 心跳 | 仍用 `sample_cpu_percent(watch_mode=False)` | PASS |
## Closure
| 项 | 状态 |
|----|------|
| CPU 曲线长期 0% | 监测模式改长窗口采样 |
| 双模心跳 | 不变,仍 5s/60s |
| 旧 Agent 2.1.0 | 可继续用,仅 CPU 显示偏 0 |
## 文件清单
- `web/agent/cpu_metrics.py`
- `web/agent/agent.py`
- `server/application/server_batch_common.py`
- `tests/test_agent_cpu_metrics.py`
## DoD
- [x] pytest cpu_metrics + watch_mode 通过
- [x] AGENT_VERSION 2.1.1
- [x] 升级脚本拉取 cpu_metrics.py
## 验证
```bash
.venv/bin/pytest tests/test_agent_cpu_metrics.py tests/test_agent_watch_mode.py -q
```
@@ -0,0 +1,39 @@
# 审计 — 统计卡「未设路径·离线」
**Changelog**: `docs/changelog/2026-07-02-stats-unset-path-offline-card.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `servers.py` | `unset_path_offline` 由 fleet/main 离线差值推导 | PASS |
| `useServerStatsCards.ts` | 6 卡顺序与字段映射 | PASS |
| `ServersPage.vue` | 点击展开未设路径 + 离线筛 | PASS |
| `DashboardPage.vue` | 跳转 query 一致 | PASS |
## Closure
| 项 | 状态 |
|----|------|
| 在线+离线≠总数 | 拆出未设路径·离线后可对齐 |
| 主列表离线筛选 | 不变,仍 8 台口径 |
## 文件清单
- `server/api/servers.py`
- `frontend/src/composables/useServerStatsCards.ts`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/pages/DashboardPage.vue`
- `tests/integration/test_servers_dashboard.py`
## DoD
- [x] API 返回 `unset_path_offline`
- [x] 前后端 6 卡展示
- [x] integration test 断言差值公式
## 验证
```bash
.venv/bin/pytest tests/integration/test_servers_dashboard.py -q
```
@@ -0,0 +1,39 @@
# 审计 — 2026-07-03 Terminal 宝塔登录按钮
**Changelog**: `docs/changelog/2026-07-03-terminal-btpanel-login-button.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `TerminalToolbar.vue` | 仅 `session` 存在时显示;loading 防重复点击 | PASS |
| `TerminalPage.vue` | 复用 `useBtPanelLogin``serverId` 取自 `activeSession` | PASS |
| `useBtPanelLogin.ts` | 未改;并发去重与 `window.open` 行为不变 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 终端内无法一键进宝塔 | FIXED | 工具栏「宝塔登录」+ 移动端菜单 |
| 与列表登录行为不一致 | N/A | 同一 composable / API |
## 文件清单
- `frontend/src/components/terminal/TerminalToolbar.vue`
- `frontend/src/pages/TerminalPage.vue`
## DoD
- [x] Step 3 规则扫描
- [x] Closure 表
- [x] 文件清单与本次批次一致
- [x] `npm run build-only` 通过
- [x] `pre_deploy_check.sh` 全门通过
## 验证
```bash
cd frontend && npm run build-only
bash deploy/pre_deploy_check.sh
bash deploy/deploy-frontend.sh
```
@@ -0,0 +1,30 @@
# 宝塔批量获取 API 并入服务器页批量栏
**日期**2026-06-21
## 变更摘要
将宝塔「全部未配置」「选中获取 API」移至 `#/servers` 批量操作栏「宝塔」分组;抽取 `useBtPanelBatchBootstrap` 与确认对话框组件。宝塔子模块列表页移除顶部重复按钮。
## 动机
运维在服务器主列表即可完成宝塔 SSH 引导,无需先切到宝塔 · 服务器列表。
## 涉及文件
- `frontend/src/composables/btpanel/useBtPanelBatchBootstrap.ts`
- `frontend/src/components/btpanel/BtPanelBatchBootstrapDialog.vue`
- `frontend/src/components/servers/ServerBatchActionBar.vue`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/components/servers/ServerUnsetPathPanel.vue`
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue`
## 迁移 / 重启
仅前端构建部署。
## 验证
1. `#/servers` 勾选服务器 → 批量栏「宝塔」见全部未配置 / 选中获取 API
2. 确认对话框启动 job,任务中心可查看进度
3. 宝塔 · 服务器列表页无顶部批量按钮,一键登录仍可用
@@ -0,0 +1,38 @@
# 宝塔一键登录:未配置时自动获取 API
**日期**2026-06-21
## 摘要
`#/servers` 与宝塔连接配置页的「一键登录」在未获取宝塔 API 时,会先通过 SSH bootstrap 写入 `api.json`,再生成临时登录链接;无需用户先点「获取 API」。
## 动机
行内一键登录应一步完成;此前未配置机器仅走 SSH 临时登录或需手动 bootstrap,与批量栏「选中获取 API」能力不一致。
## 涉及文件
- `server/application/services/btpanel_service.py``create_login_url` 未配置时 `source=immediate` bootstrap
- `frontend/src/composables/btpanel/useBtPanelLogin.ts` — 未配置时顶部 snackbar 20s 倒计时(`timeout: -1` 持久显示)
- `frontend/src/App.vue` — snackbar 支持自定义 `timeout`
- `frontend/src/api/btpanel.ts` — 响应类型含 `bootstrapped`
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue` — 移除未配置拦截,bootstrap 后刷新列表
- `tests/test_btpanel_login_url.py`
## 迁移 / 重启
无 schema 变更;需部署 API 与前端静态资源。
## 验证
```bash
pytest tests/test_btpanel_login_url.py -q
cd frontend && npx vite build
```
生产:对未配置宝塔 API 的子机点「一键登录」,应自动获取后打开面板;审计 `bt_panel_login_url` 的 detail 含 `bootstrapped: true`
## 2026-06-21 热修
- **根因**`login-url` 路由返回类型 `dict[str, str]`,响应含 `bootstrapped: bool` → FastAPI 校验 500
- **修复**`server/api/btpanel.py` 改为 `dict[str, Any]`
@@ -0,0 +1,34 @@
# 2026-06-21 宝塔系统监控 IP 白名单自愈
## 摘要
修复 `#/btpanel/monitor`(及全部宝塔代理 API)在子机返回 `{"status":false,"msg":"IP校验失败..."}` 时被当作正常 JSON 展示的问题;遇 IP 白名单失败时自动 SSH 追加中心 IP 并重试一次。
## 动机
生产验收:`GET /api/btpanel/servers/{id}/system/total` 对部分已 configured 服务器返回 HTTP 200 + 宝塔错误 JSON,前端「系统概览」卡片直接显示 `IP校验失败` 对象而非指标。根因:
1. 历史 bootstrap 曾写入 `data/api.json`,面板实际读 `config/api.json`,白名单未生效;
2. `BtPanelClient` 未识别宝塔 `status: false` 语义。
手动 `POST .../bootstrap` 可修复(`appended_whitelist`),但需用户逐台操作。
## 涉及文件
- `server/infrastructure/btpanel/client.py``raise_if_panel_error`
- `server/application/services/btpanel_service.py``proxy` IP 失败自愈 + `_repair_ip_whitelist`
- `frontend/src/pages/btpanel/BtPanelMonitorPage.vue` — 失败时清空卡片并 snackbar
- `tests/test_btpanel_client.py`
## 迁移 / 重启
- 需部署 API;无需 DB 迁移。
- 已 configured 且 SSH 可达的服务器:首次访问监控/站点等代理接口时自动补白名单。
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_client.py -q
# 生产:对曾报 IP 校验失败的服务器 GET .../system/total 应返回 memTotal/cpuNum 等指标
# 前端:#/btpanel/monitor 选服务器后应显示指标 JSON,失败时红色 snackbar 而非假数据
```
@@ -0,0 +1,23 @@
# 侧栏隐藏宝塔面板菜单
**日期**2026-06-21
## 摘要
左侧导航移除「宝塔面板」分组及全部子菜单;宝塔能力仍通过 `#/servers` 一键登录与批量获取 API 使用,`#/btpanel/*` 路由保留(直链可访问)。
## 动机
运维入口收敛到服务器页,避免侧栏重复暴露未常用宝塔管理页。
## 涉及文件
- `frontend/src/App.vue`
## 迁移 / 重启
仅前端静态资源;`cd frontend && npx vite build` 后同步 `web/app/`
## 验证
登录后侧栏无「宝塔面板」区块;`#/servers` 一键登录与批量栏「宝塔」分组正常。
@@ -0,0 +1,30 @@
# 宝塔一键登录临时时效改为 24 小时
**日期**2026-06-21
## 摘要
一键登录生成临时 `tmp_token` 时,`expire_time` 设为当前时间 + **86400 秒**24 小时);SSH 回退不再调用 `tools.py get_temp_login_ipv4`(固定 3 小时),改为远程执行同等逻辑并传入 TTL。
## 动机
用户希望登录后面板会话与临时链接上限为 24 小时;宝塔默认 3 小时,且 `tmp_login_expire` 为生成链接时的绝对时间。
## 涉及文件
- `server/infrastructure/btpanel/constants.py``BT_TEMP_LOGIN_TTL_SECONDS = 86400`
- `server/application/services/btpanel_service.py` — API `set_temp_login``expire_time`,解析 `token` 拼 URL
- `server/infrastructure/btpanel/ssh_login.py``build_ssh_temp_login_command`
- `tests/test_btpanel_temp_login_ttl.py`
## 迁移 / 重启
仅 API 变更;部署后端镜像即可,前端无改动。
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_ssh_login.py -q
```
生产:`POST /api/btpanel/servers/{id}/login-url` 后登录,约 24 小时内不应提示「临时登录已过期」(仍受面板 `session_timeout.pl` 约束)。
@@ -0,0 +1,48 @@
# 部署:本机 rsync 直传(默认不走 Gitea
**日期**2026-06-21
## 摘要
生产部署默认从本机工作区 `rsync` 到服务器,远程 `nexus-1panel.sh upgrade --skip-git` 重建镜像;不再依赖 `git push` + 远程 `git pull`
## 动机
用户要求平时不推 Gitea;部署时直接同步本机已验证代码即可。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `deploy/rsync-local-to-server.sh` | 新建:rsync 到 `/opt/nexus`,排除 `.env` / `docker/.env.prod` / `web/data` |
| `deploy/deploy-production.sh` | 默认 rsync`NEXUS_DEPLOY_VIA_GIT=1` 可恢复旧流程 |
| `deploy/nexus-1panel.sh` | `upgrade --skip-git` 跳过 git fetch/reset |
| `AGENTS.md``.cursorrules` | 部署说明更新 |
## 用法
```bash
bash deploy/pre_deploy_check.sh
bash deploy/deploy-production.sh
```
## 保护项(不同步/不改属主)
- `docker/.env.prod``web/data/``.env`、secrets
## 属主
- **子机跨传**`NEXUS_RSYNC_PUSH_CHOWN`(默认 `www:www`),见 `docs/changelog/2026-06-21-server-transfer-www-permissions.md`
- **中心机部署**:有 `www` 用户则 `www:www`,否则用部署目录现有属主(Azure 为 `azureuser:azureuser`);`NEXUS_DEPLOY_CHOWN` 可覆盖
## 传输方式
本机 `tar | ssh sudo tar -x`(不依赖远程 rsync 命令)。
## 验证
`bash -n deploy/*.sh`;用户批准后执行完整 deploy 并检查 `/health``/app/`
## 回滚
服务器保留 Docker 回滚镜像;或本机 checkout 旧版再 rsync 部署。
@@ -0,0 +1,23 @@
# 侧栏:监测历史归入系统分组
**日期**2026-06-21
## 摘要
将「监测历史」从侧栏「运维」分组移至「系统」分组(位于告警中心与审计日志之间)。
## 动机
监测历史属于系统观测/留存类功能,与命令日志、告警、审计同属系统侧栏更合理。
## 涉及文件
- `frontend/src/App.vue``opsItems` / `sysItems` 调整
## 迁移 / 重启
仅前端静态资源;路由 `#/watch-metrics` 不变。
## 验证
刷新 SPA,确认侧栏「系统」下可见「监测历史」,「运维」下已无该项。
@@ -0,0 +1,35 @@
# 跨服务器文件传输 — 审计修复
## 日期
2026-06-21
## 摘要
对跨机传输首版做走读审计,修复 deliver 失败资源泄漏、token 残留、前端模式误判与路径记忆混用。
## 审计发现与处置
| 级别 | 问题 | 处置 |
|------|------|------|
| P1 | `deliver` 拉取失败后源机 `/tmp` 归档与 Redis token 未清理 | 失败时 rollback`rm` 归档 + `delete_package_meta` |
| P1 | `deliver` 成功后 token 仍有效但源归档已删,curl 重放 404 | 成功后 `delete_package_meta` |
| P2 | 右键「发送到其他服务器」未选中行时,模式推荐误用 `selectedFiles` | 引入 `transferItems`,按实际传输项判断 |
| P2 | localStorage 直传目录与打包路径共用 key,切换模式路径错乱 | 按 `relay`/`deliver` 分 key |
| P2 | 打包成功立即关对话框,`download_url` 无法复制 | 投递成功后保留对话框至用户关闭 |
| 已知限制 | 直传多项中途失败无回滚 | v1 接受;UI 已提示大目录用打包 |
| 已知限制 | 归档与直传单文件 500MB 上限 | 与现有 download 一致,未改 |
## 涉及文件
- `server/application/services/server_file_transfer_service.py`
- `frontend/src/composables/files/useFilesActions.ts`
- `frontend/src/components/files/FilesDialogs.vue`
## 验证
```bash
.venv/bin/pytest tests/test_server_file_transfer.py -v
```
**18** 项:schema、JWT 白名单、Redis store、SFTP 大小守卫、deliver 成功/失败回滚/超限、relay 同源拒绝与成功路径、`cleanup` 命令引用。
@@ -0,0 +1,44 @@
# 跨服务器文件传输
## 日期
2026-06-21
## 摘要
文件管理器新增 A→B 跨机传输:**直传**(中心 SFTP 中继,单文件 ≤500MB)与 **打包投递**(源机 tar.gz → 签名下载 URL → 目标机 curl + 可选解压,一键完成)。
## 动机
同机剪贴板无法跨服务器;运维需对标宝塔「发送到服务器 / 打包下载」能力,避免本地下载再上传。
## 涉及文件
- `server/application/services/server_file_transfer_service.py` — 直传、打包、投递编排
- `server/infrastructure/redis/transfer_package_store.py` — 下载 token 元数据
- `server/api/sync_v2.py``server-transfer``server-transfer/deliver``transfer-download`
- `server/api/schemas.py` — 请求模型
- `server/api/auth_jwt.py` — 下载端点 JWT 豁免
- `frontend/src/composables/files/useFilesActions.ts` — 对话框与 API 调用
- `frontend/src/components/files/FilesDialogs.vue``FilesToolbar.vue``FilesList.vue`
- `frontend/src/utils/auditLabels.ts`
- `tests/test_server_file_transfer.py`
- `docs/design/specs/2026-06-21-server-file-transfer-design.md`
## 迁移 / 重启
- 需重启 API(新路由 + Redis key `transfer:pkg:*`
- 无需 DB 迁移
- 前端需 `vite build` 后部署 `web/app/`
## 验证
```bash
pytest tests/test_server_file_transfer.py -q
cd frontend && npm run type-check
bash scripts/local_verify.sh
```
- 文件页选中项 →「发送到其他服务器」→ 直传 / 打包投递
- `GET /api/sync/transfer-download/{token}` 无 JWT 可 curl;过期 404
- 审计动作:`server_file_relay``server_file_deliver`
@@ -0,0 +1,43 @@
# 打包投递:tar 覆盖解压 + 宝塔签名下载通道
**日期**2026-06-21
## 摘要
1. 解压 tar 时加 `--overwrite`,修复 `404.html: Cannot open: File exists`
2. 源机已配置宝塔 API 时,打包投递改走宝塔官方 **`/download?filename=…` 签名 URL**(B 机直拉 A 机面板),不经 Nexus 中转;效果等同自动化「下载」,**不是** UI 右键分享外链(该接口无公开 API)。
## 动机
- 站点目录已有文件时默认 tar 拒绝覆盖。
- 用户希望用宝塔下载能力;分享 `/down/{token}` 无 token API,签名 `/download` 为可自动化等价方案。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/infrastructure/btpanel/file_urls.py` | `signed_download_url` |
| `server/application/services/server_file_transfer_service.py` | `_extract_archive_on_server``_deliver_via_btpanel_download` |
| `tests/test_btpanel_file_urls.py` | 签名 URL 单测 |
| `tests/test_server_file_transfer.py` | 覆盖解压 / 宝塔投递分支 |
## 前提
- 源机 `extra_attrs.bt_panel` 已配置且 API IP 白名单允许 **B 机公网 IP** 访问面板端口。
- 未配置宝塔 API 时仍走 Nexus `transfer-download` 原流程。
## 验证
```bash
pytest tests/test_btpanel_file_urls.py tests/test_server_file_transfer.py -q
```
打包投递至已有文件的站点目录,应覆盖成功。
## 回滚
去掉 `bt_panel` 分支与 `--overwrite`,重启 API。
## 补充(2026-06-21
宝塔面板 HTTPS 多为自签证书;B 机 `curl` 下载签名 URL 时,当源机 `verify_ssl=false`(默认)自动加 `curl -k`,与 Nexus 连面板行为一致。
@@ -0,0 +1,29 @@
# 打包投递:curl 先落 /tmp 再 sudo mv
**日期**2026-06-21
## 摘要
`pull_transfer_package` / deliver 在目标机下载归档时,改为 `curl → /tmp/nexus-xfer-*``mv` 到目标路径(走 `exec_ssh_command_with_fallback` 提权),修复 `curl: (23) Failed writing body`(直写 `/www/...` 无权限)。
## 动机
打包投递时 Nexus 中心返回 200,但 B 机 `curl -o /www/...` 因 SSH 用户无写权限失败。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/application/services/server_file_transfer_service.py` | `_download_url_to_remote_path` |
| `server/api/sync_v2.py` | deliver 未捕获异常 → 502 |
| `tests/test_server_file_transfer.py` | tmp+mv 单测 |
## 验证
```bash
pytest tests/test_server_file_transfer.py::test_pull_downloads_via_tmp_then_mv -q
```
## 回滚
恢复 `curl -o dest` 直写并重启 API。
@@ -0,0 +1,20 @@
# 跨服务器传输:目标机 B 目录浏览
**日期**2026-06-21
## 摘要
「目标与方式」区域增加 B 服务器文件列表,可浏览目录;进入目录后自动同步下方目标路径(直传为目录,打包为目录下归档名)。
## 动机
仅文本框填路径不直观;需在目标机侧像源机一样点选目录确认落点。
## 涉及文件
- `frontend/src/composables/useServerFileTransfer.ts``browseDest``destCurrentPath``syncDestPathFromBrowse`
- `frontend/src/pages/ServerTransferPage.vue` — 目标机文件表
## 验证
选 B 后出现文件列表;点目录进入并更新目标路径字段;`vite build` 通过。
@@ -0,0 +1,31 @@
# 跨服务器直传:目标机写入提权
**日期**2026-06-21
## 摘要
直传(relay)在目标机写入时改用 `remote_write_bytes`SFTP 失败自动 `sudo tee`),与文件上传一致;修复目标目录无写权限时 500 且前端仅显示「操作失败」。
## 动机
生产日志:`asyncssh.sftp.SFTPPermissionDenied``dst_sftp.open(..., "wb")`;宝塔子机常见 www 目录需 sudo 写入。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/application/services/server_file_transfer_service.py` | relay 写入/建目录提权 |
| `server/api/sync_v2.py` | relay 未捕获异常 → 502 + detail |
| `frontend/src/composables/useServerFileTransfer.ts` | `formatApiError` 展示后端 detail |
## 验证
```bash
pytest tests/test_server_file_transfer.py -q
```
传输页直传至 B 机 `target_path` 下目录;失败时应显示具体原因而非「操作失败」。
## 回滚
恢复裸 SFTP `dst_sftp.open` 并重启 API。
@@ -0,0 +1,31 @@
# 跨服务器传输:目标机须手动选择
**日期**2026-06-21
## 摘要
移除传输页在选定源服务器后自动选中列表第一台作为目标机(B)的行为;B 机默认留空,用户须在目标下拉框中手动选择。
## 动机
用户反馈 B 服务器不应自动选择,避免误传到错误子机。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `frontend/src/composables/useServerFileTransfer.ts` | `onSourceServerChange``applyRouteQuery` 去掉 `candidates[0]` |
## 行为
- 从文件页跳转(`?source=&path=&paths=`)仅预填 A 机与选中路径,B 机为空。
- 换 A 机时:若当前 B 与 A 相同则清空 B;否则保留用户已选手动 B。
- URL 显式带 `?dest=``?dest_server_id=` 时仍可预填 B(深链场景)。
## 验证
打开 `#/server-transfer?source=…`,确认目标服务器下拉为空,须手动选择后方可传输。
## 回滚
恢复 `candidates[0]` 自动赋值逻辑并重新构建前端。
@@ -0,0 +1,44 @@
# 跨服务器传输独立页面
**日期**2026-06-21
## 摘要
将「跨服务器文件直传 / 打包投递」从文件管理页内嵌对话框拆分为独立 SPA 页面 `#/server-transfer`,侧栏新增入口;文件页工具栏与右键改为跳转并预填源机与路径。
## 动机
- 传输流程涉及源机浏览、多选、目标机与模式选择,独立页比对话框更易操作与扩展。
- 与文件管理职责分离:文件页专注浏览编辑,传输页专注 A→B 编排。
## 涉及文件
| 路径 | 变更 |
|------|------|
| `frontend/src/pages/ServerTransferPage.vue` | 新建完整页(源浏览 + 目标配置 + 执行) |
| `frontend/src/composables/useServerFileTransfer.ts` | 新建页 composable;支持 query `source`/`path`/`paths` |
| `frontend/src/router/index.ts` | 路由 `/server-transfer` |
| `frontend/src/App.vue` | 侧栏「跨服务器传输」 |
| `frontend/src/composables/files/useFilesActions.ts` | 移除内嵌传输逻辑,改为 `goToServerTransfer` |
| `frontend/src/composables/files/useFilesPage.ts` | 导出 `goToServerTransfer` |
| `frontend/src/components/files/FilesToolbar.vue` | 按钮跳转独立页 |
| `frontend/src/components/files/FilesDialogs.vue` | 移除传输对话框 |
| `web/app/assets/ServerTransferPage-*` | Vite 构建产物 |
## 行为说明
- 文件页选中项后点「发送到其他服务器」→ `#/server-transfer?source=&path=&paths=`(路径 `|` 分隔)。
- 独立页可手动输入绝对路径、浏览源机目录、直传或打包投递;目标路径仍用 `localStorage``nexus:file-transfer-dest:{mode}:{serverId}`
- 后端 API 不变:`POST /sync/server-transfer``POST /sync/server-transfer/deliver`
## 迁移 / 重启
- 仅前端静态资源更新,无需 DB 迁移。
- 部署:重新 `vite build` 并同步 `web/app/`
## 验证
```bash
cd frontend && npx vite build
# 浏览器:侧栏「跨服务器传输」;文件页选中后跳转预填;直传/投递 smoke
```
@@ -0,0 +1,24 @@
# 跨服务器传输页:服务器选择与搜索修复
**日期**2026-06-21
## 摘要
修复 `#/server-transfer` 源/目标服务器下拉无法选择的问题,并对齐文件页:支持按名称、域名、路径、ID 搜索过滤。
## 动机
- `v-autocomplete` 未使用 `v-model`,选项 slot 误用 `item.raw` 导致 Vuetify 4 下点击无效。
- 2000+ 台机器无客户端搜索,无法快速定位服务器。
## 涉及文件
- `frontend/src/composables/useServerFileTransfer.ts``filteredSourceServerList` / `filteredDestServerList`
- `frontend/src/pages/ServerTransferPage.vue` — 与文件页一致的选择器 UI
## 验证
```bash
cd frontend && npx vite build
# 打开 #/server-transfer:可搜索并选择源/目标服务器
```
@@ -0,0 +1,17 @@
# 跨服务器传输:源机 A 下方展示可点击网址
**日期**2026-06-21
## 摘要
选择源服务器 A 后,在服务器选择框下方显示站点域名链接(`resolveServerSiteUrl`),新标签页打开。
## 涉及文件
- `frontend/src/composables/useServerList.ts``ServerBrief` 增加 `description` / `extra_attrs`
- `frontend/src/composables/useServerFileTransfer.ts``sourceSiteUrl` / `sourceSiteHost`
- `frontend/src/pages/ServerTransferPage.vue` — 链接展示
## 说明
未 push Gitea;需上线时由用户指示再传输。
@@ -0,0 +1,28 @@
# 跨服务器传输:默认进入各机目标路径
**日期**2026-06-21
## 摘要
选源服务器 A 后浏览目录默认进入 A 的 `target_path`;选目标服务器 B 后「目标目录/归档路径」默认填入 B 的 `target_path`(切换 B 时不用旧 localStorage 覆盖)。
## 动机
与文件管理页一致:运维以各机业务根目录为起点,减少每次手动改路径。
## 涉及文件
- `frontend/src/composables/useServerFileTransfer.ts``serverTargetPath``applyDestPathForServer`
## 行为
| 操作 | 默认路径 |
|------|----------|
| 选 A | 左侧浏览 `A.target_path`(无则 `/www/wwwroot` |
| 从文件页带 `?path=` | 仍用 query 路径 |
| 选 B | 直传 → `B.target_path`;打包 → `B.target_path/xxx.tar.gz` |
| 仅切换直传/打包 | 仍可读该 B 机上次保存的路径 |
## 验证
`cd frontend && npx vite build`;页内选 A/B 观察路径字段。
@@ -0,0 +1,40 @@
# 跨服务器传输:传输后 www 权限
**日期**2026-06-21
## 摘要
跨服务器直传(relay)与打包投递(deliver/pull)完成后,在目标机上自动执行 `chown -R www:www``NEXUS_RSYNC_PUSH_CHMOD` 对应 chmod,与 rsync 推送、文件上传后权限策略一致。
## 动机
用户要求「传输过去的文件都要是 www 权限」;此前 SFTP 中继与 curl 解压只写文件,不调整属主,导致宝塔站点无法读文件。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/utils/files_upload_permissions.py` | 新增 `apply_transfer_permissions``parse_upload_dir_mode` |
| `server/application/services/server_file_transfer_service.py` | relay / pull 完成后调用权限修复 |
| `tests/test_files_upload_permissions.py` | 递归 chown/chmod 单测 |
| `tests/test_server_file_transfer.py` | relay mock `_apply_dest_permissions` |
| `deploy/rsync-local-to-server.sh` | 中心机无 `www` 用户时自动用部署目录属主 |
| `deploy/deploy-production.sh` | 同上,前端 tar 解压 chown |
## 配置
- `NEXUS_RSYNC_PUSH_CHOWN`(默认 `www:www`
- `NEXUS_RSYNC_PUSH_CHMOD`(默认 `D755,F755`
- 中心机部署属主:`NEXUS_DEPLOY_CHOWN` 可覆盖;未设置且无 `www` 用户时用 `stat` 检测
## 验证
```bash
pytest tests/test_files_upload_permissions.py tests/test_server_file_transfer.py -q
```
生产传输页直传/投递后,目标路径 `ls -la` 应为 `www:www`
## 回滚
还原上述 Python 文件并重启 API;部署脚本属主检测为向后兼容,可保留。
@@ -41,3 +41,4 @@
- 行按钮改 `small` + `tonal` + 字重 600,恢复「一键登录」全文
- 批量栏改 `flat` 实色按钮、分组标题加深,底卡用 surface 实底
- 行操作移除「删除」;删除请用批量栏「批量删除」
- 批量栏「已选 N 台」增加「选择」分组标题,与其他分区对齐
@@ -0,0 +1,30 @@
# 宝塔一键登录临时时效改为 24 小时
**日期**2026-06-22
## 摘要
一键登录生成临时 `tmp_token` 时,`expire_time` 设为当前时间 + **86400 秒**24 小时);SSH 回退不再调用 `tools.py get_temp_login_ipv4`(固定 3 小时),改为远程执行同等逻辑并传入 TTL。
## 动机
用户希望登录后面板会话与临时链接上限为 24 小时;宝塔默认 3 小时,且 `tmp_login_expire` 为生成链接时的绝对时间。
## 涉及文件
- `server/infrastructure/btpanel/constants.py``BT_TEMP_LOGIN_TTL_SECONDS = 86400`
- `server/application/services/btpanel_service.py` — API `set_temp_login``expire_time`,解析 `token` 拼 URL
- `server/infrastructure/btpanel/ssh_login.py``build_ssh_temp_login_command`
- `tests/test_btpanel_temp_login_ttl.py`
## 迁移 / 重启
仅 API 变更;部署后端镜像即可,前端无改动。
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_ssh_login.py -q
```
生产:`POST /api/btpanel/servers/{id}/login-url` 后登录,约 24 小时内不应提示「临时登录已过期」(仍受面板 `session_timeout.pl` 约束)。
@@ -0,0 +1,32 @@
# Changelog — 宝塔改端口后一键登录失败修复
**日期**2026-06-23
## 摘要
修复子机宝塔面板改端口后,Nexus「一键登录」返回「宝塔操作失败」的问题。旧 `base_url` 连接失败时现会 SSH 同步新端口并重试 API,仍失败则走 SSH 临时登录。
## 动机
用户改宝塔面板端口后,库内 `extra_attrs.bt_panel.base_url` 仍为旧端口。`httpx.ConnectError` 未被捕获,未触发既有 SSH 回退,接口直接 500。
生产日志:`POST /api/btpanel/servers/404/login-url``httpx.ConnectError: All connection attempts failed`
## 涉及文件
- `server/infrastructure/btpanel/client.py` — 网络错误包装为 `BtPanelApiError`
- `server/application/services/btpanel_service.py``_try_login_url_via_api`:失败时 `_repair_ip_whitelist``port.pl` 更新地址后重试,再 SSH 回退
- `tests/test_btpanel_login_url.py`
- `tests/test_btpanel_client.py`
## 迁移 / 重启
- 无 DB 迁移;部署后端并重启 API 容器。
## 验证
```bash
pytest tests/test_btpanel_login_url.py tests/test_btpanel_client.py -q
```
改端口子机:服务器列表 → 一键登录宝塔 → 应打开新端口临时登录页。
@@ -0,0 +1,43 @@
# 检测路径改为 nginx 网站目录
**日期**2026-06-23
## 变更摘要
批量「检测路径」由搜索 `workerman.bat` 改为读取 nginx 虚拟主机配置中的 `root` 网站目录,并写入 `target_path`
## 动机
站点目录应以 nginx 配置为准;`workerman.bat` 并非通用标志,大量服务器检测失败。
## 检测逻辑
1. 取服务器 `domain`(域名或 IP
2. SSH 远程执行:
- 优先 `/www/server/panel/vhost/nginx/{domain}.conf`(宝塔)
- 再扫描宝塔 vhost 中 `server_name` 匹配项
- 最后尝试 `/etc/nginx/sites-enabled``/etc/nginx/conf.d`
3. 解析首个 `root` 指令,校验为合法绝对路径后写入 DB
## 涉及文件
| 文件 | 变更 |
|------|------|
| `server/utils/nginx_path_detect.py` | 新增:构建远程检测命令 |
| `server/application/server_batch_common.py` | `detect_and_save_target_path` 改用 nginx |
| `frontend/src/pages/ServersPage.vue` | 确认对话框文案 |
| `scripts/batch_detect_target_path.py` | 脚本说明 |
| `tests/test_nginx_path_detect.py` | 单元测试 |
| `docs/project/nexus-functional-development-guide.md` | 功能说明 |
## 迁移 / 重启
无需 DB 迁移;后端热更新或重启 API 即可。前端需 `vite build` 后部署。
## 验证
```bash
.venv/bin/pytest tests/test_nginx_path_detect.py tests/test_server_onboarding.py -q
```
服务器页勾选多台 →「检测路径」→ 确认说明含 nginx `root` → 任务结果 `target_path → /www/wwwroot/...`
@@ -0,0 +1,36 @@
# Changelog — 订阅 IP 自动刷新 Primary 锁与 last_refresh 持久化
**日期**2026-06-23
## 摘要
修复单 worker 生产环境下 Redis 主锁竞态导致 `ip_allowlist_refresh_loop` 永不启动、进程内存订阅 IP 与 DB/实时订阅脱节的问题;将「上次刷新」时间写入 `login_subscription_last_refresh` 并广播。
## 动机
生产排查发现:
- `GET /api/settings/ip-allowlist` 返回的 `subscription_ips` 与 DB、实时拉取订阅结果不一致(内存陈旧)。
- `last_refresh` 长期为空;`nexus:primary_worker` 锁未持有,2 小时后台刷新未运行。
- 容器重启时若 Redis 仍残留旧 PID 锁,单 worker 启动一次抢锁失败后不再重试,后台任务永久禁用。
## 涉及文件
- `server/main.py` — 单 worker 跳过 Redis 选举;多 worker 启动重试抢锁
- `server/background/ip_allowlist_refresh.py` — 持久化 `login_subscription_last_refresh`
- `server/config.py` — 新 settings 键映射
- `server/api/settings.py` — 敏感键列表
- `tests/test_ip_allowlist_sync.py`
## 迁移 / 重启
- 无 DB 迁移(新 key 首次刷新时自动写入)。
- 需重启 API 容器使 primary 修复与刷新循环生效。
## 验证
```bash
pytest tests/test_ip_allowlist_sync.py tests/test_login_access.py -q
```
生产:重启后约 5s 内应完成首次订阅刷新;设置页「上次刷新」有值且节点 IP 与订阅一致。
@@ -0,0 +1,42 @@
# IP-only 服务器自动补全站点域名
**日期**: 2026-06-24
**类型**: feat(backend)
## 背景
大量子机 SSH 地址登记为 IP,服务器列表「地址」列缺少可点击站点链接。需每日自动 SSH 探测 nginx 站点名并写入元数据。
## 改动
- 新增 `server/utils/site_host.py`:判定 IP-only 与解析已有站点 host(对齐前端 `serverSiteUrl.ts`)。
- 新增 `server/utils/nginx_domain_detect.py`:远程扫描 Baota/nginx vhost 提取域名。
- 新增 batch op `detect-domain`SSH 探测并写 `extra_attrs.site_url`(不改 SSH `domain` 字段)。
- 新增定时任务 `ip_domain_detect_loop`:北京时间 **23:30** 每日触发(`IP_DOMAIN_DETECT_CRON=30 23 * * *`)。
- 配置:`IP_DOMAIN_DETECT_ENABLED`(默认 true)。
## 涉及文件
- `server/application/services/ip_domain_detect_schedule.py`
- `server/background/ip_domain_detect_loop.py`
- `server/application/server_batch_common.py`
- `server/application/services/server_batch_service.py`
- `server/config.py`, `server/main.py`
- `docs/design/specs/2026-06-24-ip-domain-detect-schedule-design.md`
- `docs/design/plans/2026-06-24-ip-domain-detect-schedule.md`
## 迁移 / 重启
- 无 DB 迁移;部署后重启 API 容器使 background loop 生效。
## 验证
```bash
pytest tests/test_site_host.py tests/test_nginx_domain_detect.py tests/test_ip_domain_detect_schedule.py -q
```
- 生产:23:30 后审计日志应出现 `batch_detect_domain``#/servers` 中 IP-only 机器地址列下方出现域名链接。
## 回滚
`.env` 设置 `IP_DOMAIN_DETECT_ENABLED=false` 并重启。
@@ -0,0 +1,77 @@
# 2026-06-24 — 未设路径区块与服务器列表对齐
## 变更摘要
调整 `#/servers` 页面「未设置路径服务器」与「服务器列表」的布局顺序与表格列宽,使两表视觉对齐并符合设计文档区块顺序。
## 动机
用户反馈未设路径区块与主列表未对齐:区块顺序与设计(主列表 → 未设路径 → 连接失败)不一致,且两张独立 `v-data-table` 列宽各自计算导致列边界错位。
## 涉及文件
- `frontend/src/pages/ServersPage.vue` — 将 `ServerUnsetPathPanel` 移至主列表卡片之后;提示文案「上方」改「下方」
- `frontend/src/components/servers/ServerUnsetPathPanel.vue` — 标题栏样式与主表一致;统一 `server-data-table` 类;修复 install/upgrade emit 类型
- `frontend/src/constants/serverTableHeaders.ts` — 补全各列 `minWidth`
- `frontend/src/styles/server-data-table.css` — 共享 `table-layout: fixed` 与展开列隐藏
- `frontend/src/main.ts` — 全局引入表格样式
## 迁移 / 重启
无。前端 rebuild 后生效。
## 验证
1. 打开 `#/servers`,确认顺序:分类筛选 → **服务器列表****未设置路径服务器** → 连接失败列表
2. 对比两表列头(状态/名称/地址/分类/目标路径/Agent/心跳/操作)左右边界对齐
3. 分类筛选后,主表为空且存在未设路径机器时,提示「已在下方区块展示」,点击「定位」滚动至未设路径卡片
---
# 2026-06-24 — 未设路径区块工具栏与主列表对齐
## 变更摘要
「未设置路径服务器」卡片标题栏右侧增加与「服务器列表」相同的搜索、添加、凭据、检测路径、列表/分类切换;保留刷新按钮。
## 动机
用户要求未设路径区块具备与主列表一致的操作入口,避免只能在上方主表标题栏操作。
## 涉及文件
- `frontend/src/components/servers/ServerListToolbarActions.vue` — 新建共享工具栏
- `frontend/src/components/servers/ServerUnsetPathPanel.vue``#toolbar` 插槽
- `frontend/src/pages/ServersPage.vue` — 主表与未设路径表共用组件
## 迁移 / 重启
无。前端 rebuild 后生效。
## 验证
1. `#/servers` 未设路径卡片标题栏可见搜索框及添加/快速添加/批量添加/凭据/检测路径/列表·分类切换/刷新
2. 在未设路径区搜索、添加服务器,主表与未设路径表同步刷新
3. 未设路径区「检测路径」按本区勾选数启用/禁用
---
# 2026-06-24 — 未设路径工具栏内嵌修复
## 变更摘要
将工具栏从 slot 改为 `ServerUnsetPathPanel` 内直接渲染;标题栏允许换行,避免 `v-card-title` 裁切右侧按钮。
## 动机
用户反馈未设路径区块仍看不到搜索/快速添加等工具栏(slot + 单行标题溢出)。
## 涉及文件
- `frontend/src/components/servers/ServerListToolbarActions.vue` — 单根容器 + flex-wrap
- `frontend/src/components/servers/ServerUnsetPathPanel.vue` — 内嵌工具栏
- `frontend/src/pages/ServersPage.vue` — v-model 绑定搜索与视图模式
## 验证
`npx vite build` 通过后,刷新 `#/servers` 向下滚动至「未设置路径服务器」,标题行右侧应可见完整工具栏。
@@ -0,0 +1,29 @@
# 宝塔 bootstrap 无变更时跳过 bt reload
**日期**2026-06-29
## 摘要
SSH bootstrap 对接宝塔 API 时,若 `api.json` 无需变更(API 已开、token 已有、中心 IP 已在白名单),**不再写盘、不执行 `/etc/init.d/bt reload`**,避免每 5 分钟重试或重复一键登录时踢掉面板已有会话。
## 动机
商城新机 `43.138.205.6` 在 Nexus bootstrap 时 `bt reload` 导致 15:32 面板全员掉线;对已配置机器重复 bootstrap 不应 reload。
## 涉及文件
- `server/infrastructure/btpanel/ssh_bootstrap.py``if actions:` 才写 `api.json` + reloadstdout 增加 `reloaded`
- `tests/test_btpanel_ssh_bootstrap.py` — 断言远程脚本条件 reload
- `docs/design/specs/2026-06-20-btpanel-module-design.md` — 设计说明同步
## 迁移 / 重启
仅后端;`deploy-production.sh` rsync + Docker `upgrade --skip-git`
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py -q
```
生产:对已 bootstrap 完成的子机再触发对接,syslog 不应再出现 `bt reload``actions` 为空时)。
@@ -0,0 +1,38 @@
# 每日离线巡检:发报前 SSH 复检 + 备注优先显示
**日期**2026-06-30
## 变更摘要
定时「每日离线巡检」Telegram 日报在统计与推送前,先对当前离线子机批量执行 SSH 健康检查并刷新 Redis 心跳,再二次扫描得到最终在线/离线数;日报正文增加「巡检前 SSH 复检」一行。离线列表与离线告警的显示名与批量任务一致:**备注 description → name → domain**。
## 动机
Agent 心跳滞后或短暂网络抖动会导致误报离线;日报发送前应主动 SSH 探测确认。离线名单在 Telegram 中应优先展示运维备注,便于识别机器。
## 涉及文件
- `server/application/server_connectivity.py``health_check_server_ids()``scan_monitored_connectivity``batch_server_display_name`
- `server/application/services/offline_daily_report_schedule.py` — 日报前 preflight 健康检查
- `server/config.py``OFFLINE_DAILY_REPORT_HEALTH_CHECK`(默认 true
- `server/infrastructure/telegram/__init__.py` — 日报增加 preflight 统计行
- `server/background/server_offline_monitor.py` — 离线告警显示名备注优先
- `tests/test_offline_daily_report_schedule.py` — preflight 流程单测
## 配置
```env
OFFLINE_DAILY_REPORT_HEALTH_CHECK=true # false 可跳过复检
```
## 迁移 / 重启
无 DB 迁移;部署后 Docker 重建 API 容器即可,background loop 随进程启动。
## 验证方式
```bash
.venv/bin/pytest tests/test_offline_daily_report_schedule.py tests/test_batch_server_display_name.py -q
```
生产:手动触发 `run_scheduled_offline_daily_report(force=True)` 或等到 11:50 北京时间;Telegram 应含复检行且离线名单显示备注。
@@ -0,0 +1,38 @@
# Agent 双模式监测:四槽 5s 出站推送,跳过 SSH
**日期**2026-07-01
## 变更摘要
单 Agent 双模式落地:
1. **常态**60s 智能心跳(变化 <10% 可 skip**30s keepalive** 拉取 `watch_active`
2. **监测**:四槽 Pin 且开启实时监测 → 中心响应 `watch_active` → Agent **5s 全量上报**(含 `load_avg``watch_mode`
3. **探针**`watch_probe_runner` 在 Agent watch 心跳新鲜时 **跳过 SSH**;进程表仍偶发 SSH 拉取;无 Agent/旧版仍 SSH 兜底
## 动机
四槽 5s SSH 探针对子机形成长期连接与频繁握手,易触发风控;子机已有 nexus-agent 出站通道,应按需切高频而非双 Agent。
## 涉及文件
- `web/agent/agent.py` — v2.1.0 双模式心跳
- `web/agent/config.example.json``heartbeat_keepalive_sec` / `watch_interval_sec`
- `server/api/agent.py` — 心跳响应 `watch_active` / `watch_interval_sec`
- `server/background/watch_probe_runner.py` — Agent 路径优先
- `server/utils/watch_metrics.py``WATCH_AGENT_FRESH_SEC`、usable 判定
- `tests/test_agent_watch_mode.py`
## 迁移
- 中心:部署 API 镜像
- 子机:**仅 Pin 监测中的机器**需升级 Agent 至 2.1.0(面板推送或 batch push
- 未升级 Agent:行为与改前相同(SSH 5s
## 验证
```bash
.venv/bin/pytest tests/test_agent_watch_mode.py tests/test_watch_metrics.py -q
```
Pin 一台已装 Agent 的子机 → 开实时监测 → 探针记录 `source=agent`;移除槽后 Agent 日志恢复 60s。
@@ -0,0 +1,54 @@
# 补录门禁:宝塔 skip-reload · Terminal 快速添加 · 快捷命令排序
**日期**2026-07-01
## 变更摘要
补录 2026-06-30 / 07-01 已上线但未经完整八道门控的批次:
1. **宝塔 bootstrap**`api.json` 无变更时不 `bt reload`,避免踢面板会话
2. **Terminal**:工具栏「快速添加(IP)」与 Servers 对齐;成功后自动 `newSession` / 跳转 Terminal
3. **Settings 快捷命令**`PUT /quick-commands/reorder` 路由置于 `/{id}` 之前,修复上下箭头无效
## 动机
- 6/30 生产部署跳过 `pre_deploy_check`(无当日 changelog/audit、本机无 :8600、Gate 8 无 cursor-agent
- 用户反馈 Settings 上下箭头点击无效应从根因修复并补门禁
## 涉及文件
**后端**
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `server/api/terminal.py`
- `tests/test_btpanel_ssh_bootstrap.py`
- `tests/test_terminal_quick_commands.py`
**前端**
- `frontend/src/composables/servers/useServerQuickAdd.ts`
- `frontend/src/components/servers/ServerQuickAddDialogs.vue`
- `frontend/src/utils/openServerTerminal.ts`
- `frontend/src/components/terminal/TerminalTabBar.vue`
- `frontend/src/pages/TerminalPage.vue`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/components/TerminalQuickCommandsSettings.vue`
**门禁**
- `deploy/pre_deploy_check.sh` — Gate 3 本机 :8600 不可达时回退 pytest 专项
- `docs/changelog/2026-06-29-btpanel-bootstrap-skip-reload.md`(6/29 变更说明,一并纳入本批验证)
## 迁移 / 重启
- 宝塔 skip-reloadNexus 中心 `deploy-production.sh` 重建 API 镜像
- Terminal / 快捷命令:前端 `vite build` + 同步 `web/app`
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py -q
bash deploy/pre_deploy_check.sh
```
生产:Terminal 快速添加 → 自动开 SSH 标签;Settings 快捷命令上下箭头可调序。
@@ -0,0 +1,35 @@
# Agent 2.1.1:监测模式 CPU 采样优化
**日期**2026-07-02
## 变更摘要
修复四槽实时监测下 CPU 曲线长期显示 **0%** 的问题:
1. 新增 `web/agent/cpu_metrics.py`:监测模式用 `interval=None` 测约 **5s 窗口**(与心跳间隔对齐);首次进入 watch 用 **1s bootstrap**
2. `agent.py` 升至 **v2.1.1**`_build_system_info` 改用 `sample_watch_cpu_bundle`(含 `per_cpu_pct`
3. 升级/安装脚本同步下载 `cpu_metrics.py`
## 动机
v2.1.0 监测模式用 `psutil.cpu_percent(interval=0.3)`,低负载机器常四舍五入为 0;内存/负载正常,属采样窗口过短而非探针故障。
## 涉及文件
- `web/agent/cpu_metrics.py`(新建)
- `web/agent/agent.py` — v2.1.1
- `server/application/server_batch_common.py` — curl 下载 cpu_metrics
- `tests/test_agent_cpu_metrics.py`(新建)
## 迁移
- 中心:部署后 `/agent/cpu_metrics.py` 可访问
- 子机:**Pin 且开实时监测**的机器点槽内「升级 Agent」至 2.1.1
## 验证
```bash
.venv/bin/pytest tests/test_agent_cpu_metrics.py tests/test_agent_watch_mode.py -q
```
升级后探针 `cpu_pct` 在低负载下应出现非零小幅波动,不再长期贴 0。
@@ -0,0 +1,29 @@
# 统计卡:新增「未设路径·离线」
**日期**2026-07-02
## 变更摘要
仪表盘 / 服务器页顶部统计由 5 卡扩为 6 卡,新增 **未设路径·离线**
- 口径:`unset_path_offline = 全库 Agent 离线 主列表离线`
- 使 **在线 + 离线 + 未设路径·离线 ≈ 服务器总数**(消除原先 390+8≠428 的困惑)
- 点击跳转 `/servers?unset_path=1&status=offline`,定位未设路径区块并筛离线
## 动机
在线卡统计全库 Agent 在线(含未设路径),离线卡仅主列表离线;约 30 台「未设路径且离线」无处展示,用户误以为统计错误。
## 涉及文件
- `server/api/servers.py``unset_path_offline` 字段
- `frontend/src/composables/useServerStatsCards.ts`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/pages/DashboardPage.vue`
- `tests/integration/test_servers_dashboard.py`
## 验证
```bash
.venv/bin/pytest tests/integration/test_servers_dashboard.py -q
```
@@ -0,0 +1,29 @@
# Terminal 工具栏一键登录宝塔
**日期**2026-07-03
## 变更摘要
在 WebSSH 终端页,当已打开服务器会话时,于工具栏「字号」左侧增加 **宝塔登录** 按钮;复用服务器列表同一套 `useBtPanelLogin` 逻辑,对当前激活标签的 `serverId` 生成并打开宝塔面板登录链接。
## 动机
运维在终端排障时常需切到宝塔面板,此前只能回服务器列表点「一键登录」;在终端内直接登录可减少跳转。
## 涉及文件
**前端**
- `frontend/src/components/terminal/TerminalToolbar.vue` — 桌面按钮 + 移动端齿轮菜单项
- `frontend/src/pages/TerminalPage.vue` — 接入 `useBtPanelLogin`、loading 与点击处理
## 迁移 / 重启
- 仅前端静态资源:`bash deploy/deploy-frontend.sh`Docker 生产走 `nexus-1panel.sh upgrade` 重建镜像内前端)
## 验证
```bash
cd frontend && npm run build-only
# 打开 #/terminal,连接任意服务器后工具栏应显示「宝塔登录」
```
@@ -0,0 +1,27 @@
# 告警连续超阈门控(C2
**日期**2026-07-04
## 变更
CPU/内存/磁盘告警需 **连续 N 次**(默认 **3**)心跳超阈才触发 `broadcast_alert`;单次尖峰不再入库/Telegram。
| 项 | 说明 |
|----|------|
| 配置 | `alert_streak_required`(设置页「连续超阈次数」,默认 3) |
| 常态心跳 60s | 约 **3 分钟**持续超阈才告警 |
| 监测槽 5s | 约 **15 秒**持续超阈才告警 |
| 设为 1 | 恢复旧行为(单次超阈即告警) |
| Redis | `alert_streak:{server_id}:{metric}`,回落正常后清零 |
## 文件
- `server/utils/alert_metrics.py` — 阈值检测共用
- `server/utils/alert_streak.py` — 连续计数 + 告警/recovery 门控
- `server/api/agent.py` — 心跳接入
- `server/utils/watch_alerts.py` — 监测探针接入
- `tests/test_alert_streak.py`
## 部署
中心机 `nx update``deploy-production.sh`**无需**子机 Agent 升级。
@@ -0,0 +1,30 @@
# 检测路径改为搜索 crmeb 目录
**日期**2026-07-05
## 变更摘要
批量「检测路径」由在 `/www/wwwroot` 下搜索 `workerman.bat` 改为搜索名为 `crmeb` 的目录,并将其完整路径写入 `target_path`
## 检测逻辑
```bash
find /www/wwwroot -iname 'crmeb' -type d -print -quit 2>/dev/null
```
示例:`/www/wwwroot/shop.example.com/crmeb``target_path` = `/www/wwwroot/shop.example.com/crmeb`
## 涉及文件
| 文件 | 变更 |
|------|------|
| `server/application/server_batch_common.py` | `detect_and_save_target_path` |
| `frontend/src/pages/ServersPage.vue` | 确认对话框文案 |
| `scripts/batch_detect_target_path.py` | 脚本说明 |
| `tests/test_nginx_path_detect.py` | 单元测试 |
## 验证
```bash
.venv/bin/pytest tests/test_nginx_path_detect.py -q
```
@@ -0,0 +1,33 @@
# 宝塔 bootstrap:真安装检测 + config 目录自愈
**日期**2026-07-08
## 摘要
SSH bootstrap 写 `api.json` 前校验 `class/common.py` 存在(空 `panel` 目录不再误判为已装宝塔);写盘前 `makedirs` config 目录,避免 `FileNotFoundError` 被归类为模糊的 `ssh_command_failed`
## 动机
新机 **山西鑫途汇**`42.193.230.144`)入库时仅有空 `/www/server/panel/`,一键登录反复 `ssh_command_failed`;同公司 M 机宝塔正常。根因是远程脚本只检查 `panel` 目录存在,写 `config/api.json` 时父目录缺失崩溃。
## 涉及文件
- `server/infrastructure/btpanel/ssh_bootstrap.py``common.py` 检测、`os.makedirs` config
- `tests/test_btpanel_ssh_bootstrap.py` — 远程脚本断言
## 迁移 / 重启
仅后端;`rsync-local-to-server.sh` + Docker `upgrade --skip-git`
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py -q
```
生产:对 `bt_installed=false` 的机器一键登录应返回 `bt_not_installed`installing 状态),不再 `ssh_command_failed`
## 2026-07-08 热修(回归)
- **根因**2026-06-21「一键登录自动 bootstrap」在 `bootstrap` 失败时直接 `raise`,不再走 SSH `tools.py` 临时登录;新机 API 未就绪时整链中断。
- **修复**`create_login_url` 对非凭据类 bootstrap 错误(如 `ssh_command_failed`)记录 warning 后 **继续 SSH 回退**;仅 `ssh_auth_failed` / `ssh_no_credentials` 等硬失败仍中断。
@@ -0,0 +1,28 @@
# 宝塔未安装时一键登录友好提示
**日期**2026-07-09
## 摘要
未安装宝塔的子机点击「宝塔一键登录」时,不再抛出 Python `ModuleNotFoundError: public` 堆栈,改为明确中文:`该机未检测到宝塔面板…`
## 动机
运维误对无宝塔机器点登录,SSH 回退脚本 `import public` 失败,前端只显示晦涩 Traceback。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/infrastructure/btpanel/ssh_login.py` | `BtPanelNotInstalledError``humanize_ssh_login_error`、SSH 前检测 `common.py` |
| `server/application/services/btpanel_service.py` | bootstrap `bt_not_installed` 不再回退 SSH |
| `server/api/btpanel.py` | 400 + 友好 `detail` |
| `tests/test_btpanel_ssh_login.py` | 未安装 / public 模块报错映射 |
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_login.py tests/test_btpanel_temp_login_ttl.py -q
```
对未装宝塔机器 `POST /api/btpanel/servers/{id}/login-url` → HTTP 400,detail 含「未检测到宝塔面板」。
@@ -0,0 +1,33 @@
# 跨服务器文件传输 — 实施计划
## 涉及文件
| 文件 | 变更 |
|------|------|
| `server/infrastructure/redis/transfer_package_store.py` | 新建:打包任务 Redis |
| `server/application/services/server_file_transfer_service.py` | 新建:直传 / 打包 / 拉取 / 流 |
| `server/api/schemas.py` | 请求模型 |
| `server/api/sync_v2.py` | 四个端点 |
| `server/api/auth_jwt.py` | 下载端点 JWT 豁免 |
| `frontend/src/composables/files/useFilesActions.ts` | 对话框逻辑 |
| `frontend/src/components/files/FilesDialogs.vue` | UI |
| `frontend/src/components/files/FilesList.vue` | 右键菜单 |
| `tests/test_server_file_transfer.py` | 单元测试 |
| `docs/changelog/2026-06-21-server-file-transfer.md` | 变更记录 |
## 步骤
1. Redis store + serviceSFTP 递归中继、远程 tar、目标 curl)
2. API + 审计
3. 前端对话框(模式切换、目标服务器、路径、打包后展示链接与一键 pull)
4. pytest + `scripts/local_verify.sh` 相关用例
## 回滚
删除新端点与服务文件;Redis key `transfer:pkg:*` 自然过期。无需 DB 迁移。
## 测试要点
- 校验:同源目标、空 sources、路径非法
- token 过期 / 不存在 → 404
- mock SFTP 的 relay 字节计数(可选)
@@ -0,0 +1,37 @@
# IP 域名自动探测 — 实施说明
**日期**: 2026-06-24
## 涉及文件
| 文件 | 动作 |
|------|------|
| `server/utils/site_host.py` | 新增:IP 判定与站点 host 解析 |
| `server/utils/nginx_domain_detect.py` | 新增:远程 nginx 域名探测命令 |
| `server/application/server_batch_common.py` | 扩展:探测并写 site_url |
| `server/application/services/ip_domain_detect_schedule.py` | 新增 |
| `server/background/ip_domain_detect_loop.py` | 新增 |
| `server/application/services/server_batch_service.py` | 新增 op `detect-domain` |
| `server/config.py` | 新增配置项 |
| `server/main.py` | 注册 loop |
| `tests/test_*.py` | 单元测试 |
| `docs/changelog/2026-06-24-ip-domain-detect-schedule.md` | changelog |
## 实现步骤
1. `site_host.py` 镜像前端 `serverSiteUrl.ts` 逻辑。
2. `build_nginx_domain_detect_command` 扫描 Baota vhost 文件名 + server_name。
3. `detect_and_save_site_url` SSH 执行并更新 `extra_attrs`
4. schedule 列出候选 → `start_batch_job(op="detect-domain")`
5. main 注册 loopconfig 默认 23:30。
## 测试要点
- cron 23:30 北京时区匹配
- `list_ip_only_without_site_server_ids` 过滤逻辑
- nginx 命令含 quote、跳过 IP basename
- schedule 幂等(already_ran_today
## 回滚
- `.env``IP_DOMAIN_DETECT_ENABLED=false` 并重启 API;或删除新增 loop 注册。
@@ -44,7 +44,7 @@
1. 无面板目录 → `bt_not_installed`(状态 `installing`5min 重试)
2. 读/写 `api.json`:保留 token、开启 API、追加中心 IP 到 `limit_addr`
3. 原子写 + `bt reload`stdout JSON → 加密写入 `api_key_enc` + `base_url`
3. 仅当 `api.json` 有变更(新 token / 开 API / 追加白名单)时原子写 + `bt reload`;已配置则跳过 reload,避免踢面板会话stdout JSON → 加密写入 `api_key_enc` + `base_url`
### 状态机(`extra_attrs.bt_panel`
@@ -0,0 +1,50 @@
# 跨服务器文件传输设计
## 背景与目标
文件管理器当前剪贴板仅支持**同一台服务器**内复制/移动。运维需要在 A 服务器选定文件或目录后,传到 B 服务器,无需经本地下载再上传。
对标宝塔「发送到服务器 / 打包下载链接」能力,提供两种模式:
| 模式 | 适用 | 数据路径 |
|------|------|----------|
| **直传 relay** | 单文件、多文件(含目录,递归 SFTP) | A → Nexus 中心 SFTP 中继 → B |
| **打包 package** | 大目录、批量归档 | A 上 tar.gz → Nexus 签名下载 URL → B `curl` 拉取(可解压) |
## 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| A. 中心 SFTP 中继 | 不依赖 A/B 互通;复用现有 SSH 凭据 | 带宽经中心;超大文件受 500MB/文件限制 |
| B. A 上 scp 到 B | 不经中心 | 需在 A 存 B 凭据;子机间常不通 |
| C. 仅打包 + wget | 适合目录;B 直连 API | 需临时公开下载端点(签名 token) |
**选定**:A(直传)+ C(打包拉取)组合;拒绝 B。
## 接口
前缀 `/api/sync``sync_v2.py`)。
| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| POST | `/server-transfer` | JWT | 直传 |
| POST | `/server-transfer/deliver` | JWT | 一键打包 + B 拉取 + 可选解压 |
| POST | `/server-transfer/package` | JWT | 仅打包(高级) |
| POST | `/server-transfer/pull` | JWT | 仅拉取(高级) |
| GET | `/transfer-download/{token}` | **token**(无 JWT | 流式下载归档 |
## 安全
- `source_server_id != dest_server_id`
- 路径校验复用 `assert_clipboard_transfer_safe` / `coerce_remote_abs_path`
- 下载 token 存 RedisTTL 1h;路径仅 token 可查
- `GET /transfer-download/` 加入 JWT 白名单前缀;token 即凭证
- 单文件直传上限 500MB(与现有下载一致)
- 全操作写 `audit_logs`
## 验收标准
- [ ] 文件页选中 1+ 项 →「发送到其他服务器」→ 直传到 B 指定目录
- [ ] 打包模式生成可 curl 的 URL;B 机 pull 成功并可解压
- [ ] 源=目标、非法路径返回 400
- [ ] pytest 覆盖校验与 token store
@@ -0,0 +1,44 @@
# IP 服务器自动补全站点域名 — 设计
**日期**: 2026-06-24
## 背景与目标
部分子机 SSH 地址(`servers.domain`)登记为 IP,列表「地址」列无法展示可点击站点链接。运维曾用手工脚本写 `extra_attrs.site_url`;需每日自动补全。
**目标**:北京时间每天 23:30,对「domain 为 IP 且尚无站点域名」的服务器 SSH 探测 nginx 站点名,写入 `extra_attrs.site_url`(不改 SSH 连接字段)。
## 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| A. SSH 读 Baota/nginx vhost | 与 detect-path 一致,无面板依赖 | 需 SSH 凭据 |
| B. 宝塔 API list_sites | 结构化 | 需先 bootstrap 面板凭据,覆盖不全 |
| C. 仅读 target_path 推断 | 无 SSH | 大量机器 target_path 仍为默认 |
**选定 A**:复用现有 SSH 池与 batch job 框架;target_path 仅作优选 hint。
## 数据模型
- **输入**`Server.domain` 为 IPv4/IPv6`resolve_server_site_host(server)` 为空。
- **输出**`extra_attrs.site_url = "<hostname>"`(裸域名,与现有运维脚本一致)。
- **不修改**`domain`SSH 仍走 IP)。
## 接口与任务
- 新 batch op`detect-domain`
- 新后台 loop`ip_domain_detect_loop`60s 轮询 cron
- 配置:`IP_DOMAIN_DETECT_ENABLED`(默认 true)、`IP_DOMAIN_DETECT_CRON`(默认 `30 23 * * *`
## 安全与性能
- 远程命令经 `shlex.quote`;只读 nginx 配置。
- 并发沿用 batch `CONCURRENCY=5`;每日 Redis 去重一次。
- 仅 primary worker 执行。
## 验收标准
1. cron `30 23 * * *` 在北京时间 23:30 触发一次 batch。
2. IP-only 且无 site_url 的服务器被纳入;已有 site_url 的跳过。
3. 成功探测后列表 `#/servers` 地址列 IP 下出现可点击域名。
4. pytest 覆盖 cron、候选筛选、命令构建。
@@ -266,11 +266,11 @@ Header: `X-API-Key` 或 queryAgent 脚本约定)。
| `SshKeyPreset` | `ssh_key_presets` | SSH 密钥模板 | `encrypted_private_key` |
| `PushSchedule` | `push_schedules` | 定时任务 | `cron_expr`, `server_ids`, `schedule_type` push/script |
| `PushRetryJob` | `push_retry_jobs` | 失败重试 | 指数退避, `max_retries=3` |
| `AuditLog` | `audit_logs` | 审计 | `action`, `target_*`, `ip_address` |
| `AuditLog` | `audit_logs` | 审计**7 天**自动清理) | `action`, `target_*`, `ip_address` |
| `Script` | `scripts` | 脚本库 | `content`, `category` |
| `ScriptExecution` | `script_executions` | 执行记录 | `results` JSON, `status` |
| `DbCredential` | `db_credentials` | DB 凭据 | 脚本内 `$DB_*` 替换 |
| `AlertLog` | `alert_logs` | 告警历史 | `is_recovery` |
| `AlertLog` | `alert_logs` | 告警历史**7 天**自动清理) | `is_recovery` |
| `SshSession` | `ssh_sessions` | WebSSH 会话 | UUID `id` |
| `CommandLog` | `command_logs` | 终端命令 | `command` ≤2000 字符 |
| `QuickCommand` | `quick_commands` | 终端快捷命令 | `sort_order` |
@@ -314,6 +314,8 @@ Agent(60s) → POST /api/agent/heartbeat
| `retry_runner` | 300s | `background/retry_runner.py` | `push_retry_jobs` SKIP LOCKED 重试 |
| `ip_allowlist_refresh` | 7200s | `background/ip_allowlist_refresh.py` | 订阅 URL 更新白名单 |
| `upload_staging_cleanup` | 3600s | `background/upload_staging_cleanup.py` | 清理 `/tmp/nexus_upload_*` >24h |
| `alert_log_purge` | 24h | `background/alert_log_purge.py` | 清理 `alert_logs` >7d |
| `audit_log_purge` | 24h | `background/audit_log_purge.py` | 清理 `audit_logs` >7d |
---
@@ -340,7 +342,7 @@ Agent(60s) → POST /api/agent/heartbeat
### 9.3 服务器 `/api/servers`
CRUD、stats、categories、logs、CSV import/export、batch agent install/upgrade/uninstall、**batch detect-path**SSH 搜索 `workerman.bat` 自动写入 `target_path`)、health check、files-capability、setup-files-sudo、单台 agent 操作。
CRUD、stats、categories、logs、CSV import/export、batch agent install/upgrade/uninstall、**batch detect-path**SSH `/www/wwwroot` 下搜索 `crmeb` 目录写入 `target_path`)、health check、files-capability、setup-files-sudo、单台 agent 操作。
### 9.4 同步 `/api/sync``sync_v2.py`,核心)
@@ -537,7 +539,7 @@ main.ts → App.vue(侧栏/搜索)→ router-view
| **连接失败列表** | `GET /servers/pending` |
| **失败重试 / 删除** | `POST /servers/pending/{id}/retry``DELETE /servers/pending/{id}` |
| 批量选择 | 安装/升级/卸载 Agent、**检测目标路径**、健康检查、删除 |
| **检测目标路径** | `POST /servers/batch/detect-path` — Body: `{ server_ids }`SSH 在 `/www/wwwroot` `workerman.bat`,将所在目录写入 `target_path`;结果弹窗逐台展示 |
| **检测目标路径** | `POST /servers/batch/detect-path` — Body: `{ server_ids }`SSH 在 `/www/wwwroot`搜索 `crmeb` 目录写入 `target_path`;结果弹窗逐台展示 |
| **列表快捷编辑** | 目标路径列 ✏️ → `PUT /servers/{id}` `{ target_path }` |
| CSV 导出 | 客户端生成(无密码列) |
| CSV 导入 | `POST /servers/import` FormData |
@@ -569,6 +571,16 @@ main.ts → App.vue(侧栏/搜索)→ router-view
| Editor | Monaco`POST /sync/read-file|write-file` |
| 权限 | files-capability 检测、sudo 提示 |
| 热键 | 复制/粘贴/删除(`useFilesHotkeys` |
| 跨机传输 | 工具栏/右键跳转 `#/server-transfer`(见 §12.4.1 |
### 12.4.1 ServerTransferPage `/server-transfer`
| 能力 | 说明 |
|------|------|
| 源机浏览 | `POST /sync/browse`,多选 + 手动绝对路径 |
| 直传 | `POST /sync/server-transfer`(单文件 ≤500MB |
| 打包投递 | `POST /sync/server-transfer/deliver` + 可选解压 |
| 深链 | `?source=&path=&paths=` 自文件页预填 |
### 12.5 PushPage `/push`
@@ -0,0 +1,53 @@
# Codex 审批代理修复说明(2026-07-08
## 问题
当前线程的高权限命令审批使用 `auto_review`,它会请求 `codex-auto-review` 模型。
你的 CC Switch / custom provider 当前只支持 `gpt-5.5`,不支持 `codex-auto-review`,所以 SSH、scp、联网验证都会被拒绝。
## 修复方式
把当前线程的审批人从:
```json
"approvalsReviewer": "auto_review"
```
改成:
```json
"approvalsReviewer": "user"
```
这样以后高权限命令会弹给你手动确认,而不是走坏掉的自动审核模型。
## 我已生成脚本
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\fix-codex-approval-reviewer.ps1
```
## 运行方法
在 PowerShell 里执行:
```powershell
Set-ExecutionPolicy -Scope Process Bypass
& "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\fix-codex-approval-reviewer.ps1"
```
脚本会自动备份:
```text
C:\Users\uzuma\.codex\.codex-global-state.json.bak-switch-approval-时间戳
```
## 运行后
建议重启 Codex,或者关闭再打开当前线程。然后我就可以再次尝试:
```cmd
python scripts\publish_release_bundle.py --remote nexus --tarball ... --sha256 ...
```
届时应该会弹出手动审批,而不是再报 `codex-auto-review` 不支持。
@@ -0,0 +1,380 @@
# Nexus API / Service / 数据库调用链索引报告
生成时间:2026-07-07
## 1. 总览
| 项目 | 数量 |
| --- | --- |
| API 路由 | 214 |
| Service 方法 | 324 |
| 调用边 | 16780 |
| 数据库调用点 | 437 |
| Redis 调用点 | 729 |
| SSH/远程命令调用点 | 346 |
| 宝塔相关流 | 183 |
## 2. API 方法分布
| HTTP 方法 | 数量 |
| --- | --- |
| POST | 104 |
| GET | 77 |
| PUT | 18 |
| DELETE | 13 |
| PATCH | 2 |
## 3. API 风险标签分布
| 风险标签 | 路由数 |
| --- | --- |
| file | 37 |
| ssh | 35 |
| script | 34 |
| btpanel | 28 |
| install | 18 |
| login | 11 |
| terminal | 7 |
## 4. 高优先级 API 路由(按风险标签/服务调用筛选)
| 方法 | 路径 | 风险标签 | 入口 | 函数 | Service 调用候选 |
| --- | --- | --- | --- | --- | --- |
| POST | /script-callback | script | server/api/agent.py:245 | script_job_callback | |
| GET | /nodes | script | server/api/assets.py:126 | list_nodes | |
| GET | /ssh-sessions | script,ssh | server/api/assets.py:230 | list_ssh_sessions | |
| GET | /command-logs | script,ssh | server/api/assets.py:282 | list_command_logs | |
| GET | /login-access | login | server/api/auth.py:116 | login_access | |
| POST | /login | login | server/api/auth.py:128 | login | service.login |
| POST | /refresh | | server/api/auth.py:173 | refresh_token | service.refresh_token |
| POST | /logout | | server/api/auth.py:214 | logout | service.logout_by_token |
| POST | /totp/setup | | server/api/auth.py:235 | setup_totp | service.setup_totp |
| POST | /totp/enable | | server/api/auth.py:258 | enable_totp | service.enable_totp |
| POST | /totp/disable | | server/api/auth.py:280 | disable_totp | service.disable_totp |
| PUT | /password | login | server/api/auth.py:303 | change_password | AuthService, auth_service._delete_all_refresh_tokens, AuthService._verify_totp |
| POST | /webssh-token | ssh | server/api/auth.py:363 | issue_webssh_token | service.create_webssh_token |
| GET | /me | login | server/api/auth.py:385 | get_me | |
| GET | /servers | btpanel | server/api/btpanel.py:60 | list_bt_servers | |
| GET | /servers/{server_id}/config | btpanel,install,script,ssh | server/api/btpanel.py:68 | get_bt_config | |
| PUT | /servers/{server_id}/config | btpanel | server/api/btpanel.py:81 | update_bt_config | |
| GET | /settings | btpanel | server/api/btpanel.py:103 | get_bt_global_settings | |
| PUT | /settings | btpanel | server/api/btpanel.py:111 | update_bt_global_settings | |
| POST | /servers/{server_id}/bootstrap | btpanel | server/api/btpanel.py:129 | bootstrap_bt_server | |
| POST | /servers/batch-bootstrap | btpanel | server/api/btpanel.py:148 | batch_bootstrap_bt_servers | |
| POST | /servers/{server_id}/login-url | btpanel,login | server/api/btpanel.py:166 | create_bt_login_url | |
| GET | /servers/{server_id}/system/total | btpanel | server/api/btpanel.py:183 | bt_system_total | |
| GET | /servers/{server_id}/system/disk | btpanel | server/api/btpanel.py:191 | bt_system_disk | |
| GET | /servers/{server_id}/system/network | btpanel | server/api/btpanel.py:199 | bt_system_network | |
| GET | /servers/{server_id}/sites | btpanel | server/api/btpanel.py:207 | bt_list_sites | |
| POST | /servers/{server_id}/sites/start | btpanel | server/api/btpanel.py:215 | bt_site_start | |
| POST | /servers/{server_id}/sites/stop | btpanel | server/api/btpanel.py:228 | bt_site_stop | |
| GET | /servers/{server_id}/sites/php-versions | btpanel | server/api/btpanel.py:241 | bt_php_versions | |
| POST | /servers/{server_id}/sites | btpanel | server/api/btpanel.py:249 | bt_create_site | |
| GET | /servers/{server_id}/sites/{site_id}/domains | btpanel | server/api/btpanel.py:280 | bt_list_domains | |
| POST | /servers/{server_id}/domains | btpanel | server/api/btpanel.py:293 | bt_add_domain | |
| POST | /servers/{server_id}/domains/delete | btpanel | server/api/btpanel.py:306 | bt_delete_domain | |
| GET | /servers/{server_id}/ssl/sites | btpanel | server/api/btpanel.py:319 | bt_ssl_sites | |
| POST | /servers/{server_id}/ssl/set | btpanel | server/api/btpanel.py:327 | bt_set_ssl | |
| POST | /servers/{server_id}/ssl/apply | btpanel | server/api/btpanel.py:345 | bt_apply_ssl | |
| GET | /servers/{server_id}/databases | btpanel | server/api/btpanel.py:358 | bt_list_databases | |
| POST | /servers/{server_id}/databases | btpanel | server/api/btpanel.py:366 | bt_create_database | |
| POST | /servers/{server_id}/databases/password | btpanel | server/api/btpanel.py:384 | bt_db_password | |
| POST | /servers/{server_id}/databases/backup | btpanel | server/api/btpanel.py:397 | bt_db_backup | |
| GET | /servers/{server_id}/crontab | btpanel | server/api/btpanel.py:410 | bt_crontab | |
| POST | /servers/{server_id}/services | btpanel | server/api/btpanel.py:418 | bt_service_admin | _svc.service_admin |
| GET | '' | script | server/api/execution_records.py:20 | list_records | |
| GET | /{record_id} | script | server/api/execution_records.py:41 | get_record_detail | |
| GET | /browse | file,ssh | server/api/files.py:40 | browse_directory_get | |
| GET | /status | install | server/api/install.py:972 | install_status | |
| GET | /env-check | install | server/api/install.py:1006 | env_check | |
| POST | /test-credentials | install | server/api/install.py:1097 | test_credentials | |
| GET | /connection-check | file,install | server/api/install.py:1113 | connection_check | |
| POST | /init-db | install | server/api/install.py:1153 | init_db | |
| POST | /create-admin | install | server/api/install.py:1327 | create_admin | |
| POST | /lock | install | server/api/install.py:1426 | lock_install | |
| GET | /state | file,install | server/api/install.py:1469 | get_install_state | |
| GET | / | script | server/api/scripts.py:31 | list_scripts | service.list_scripts |
| GET | /exec-config | script | server/api/scripts.py:42 | script_exec_config | |
| GET | /credentials | script | server/api/scripts.py:56 | list_credentials | service.list_credentials |
| POST | /credentials | script | server/api/scripts.py:66 | create_credential | service.create_credential |
| PUT | /credentials/{id} | script | server/api/scripts.py:90 | update_credential | |
| DELETE | /credentials/{id} | script | server/api/scripts.py:131 | delete_credential | service.delete_credential |
| GET | /executions | script | server/api/scripts.py:152 | list_executions | service.list_executions |
| POST | /executions/{id}/stop | script | server/api/scripts.py:167 | stop_execution | service.stop_execution, service.get_execution_detail |
| POST | /executions/{id}/mark-stuck | script | server/api/scripts.py:181 | mark_execution_stuck | service.mark_execution_stuck, service.get_execution_detail |
| POST | /executions/{id}/retry | script | server/api/scripts.py:198 | retry_execution | service._parse_execution_meta, service.get_execution, service.get_execution_detail, service.retry_execution |
| GET | /executions/{id} | script | server/api/scripts.py:238 | get_execution | service.get_execution_detail |
| GET | /{id} | script | server/api/scripts.py:258 | get_script | service.get_script |
| POST | / | script | server/api/scripts.py:271 | create_script | service.create_script |
| PUT | /{id} | script | server/api/scripts.py:295 | update_script | service.get_script, service.update_script |
| DELETE | /{id} | script | server/api/scripts.py:325 | delete_script | service.get_script, service.delete_script |
| POST | /exec | script | server/api/scripts.py:364 | execute_command | service.get_execution_detail, service.execute_command |
| GET | / | script | server/api/search.py:32 | global_search | |
| GET | / | script | server/api/servers.py:94 | list_servers | |
| GET | /terminal-search-history | terminal | server/api/servers.py:436 | get_terminal_search_history | |
| PUT | /terminal-search-history | terminal | server/api/servers.py:454 | update_terminal_search_history | |
| GET | /logs | script | server/api/servers.py:491 | get_all_sync_logs | |
| GET | /meta/api_base_url | install | server/api/servers.py:561 | get_api_base_url | |
| GET | /import/template | file | server/api/servers.py:570 | download_import_template | |
| POST | /import | file | server/api/servers.py:589 | import_servers | service.create_server |
| POST | /batch/install-agent | install | server/api/servers.py:832 | batch_install_agent | |
| POST | /batch/detect-path | ssh | server/api/servers.py:850 | batch_detect_path | |
| POST | /batch/uninstall-agent | install | server/api/servers.py:859 | batch_uninstall_agent | |
| GET | /pending | script,ssh | server/api/servers.py:977 | list_pending_servers | |
| POST | /add-by-ip | login,ssh | server/api/servers.py:1099 | add_server_by_ip | |
| GET | /{id} | | server/api/servers.py:1334 | get_server | service.get_server |
| GET | /{id}/metrics | | server/api/servers.py:1362 | get_server_metrics | service.get_server |
| GET | /{id}/files-capability | file,ssh | server/api/servers.py:1407 | get_files_capability | service.get_server |
| POST | /{id}/setup-files-sudo | file,install,ssh | server/api/servers.py:1422 | setup_files_sudo | service.get_server |
| POST | / | file,ssh | server/api/servers.py:1463 | create_server | service.create_server |
| PUT | /{id} | file,ssh | server/api/servers.py:1537 | update_server | service.get_server, service.update_server |
| DELETE | /{id} | | server/api/servers.py:1629 | delete_server | service.get_server, service.delete_server |
| POST | /{id}/agent-key | | server/api/servers.py:1659 | generate_agent_api_key | service.get_server, service.update_server |
| POST | /{id}/install-agent | install,ssh | server/api/servers.py:1711 | install_agent_remote | service.get_server, service.update_server |
| POST | /{id}/uninstall-agent | file,install,ssh | server/api/servers.py:1818 | uninstall_agent_remote | service.get_server |
| POST | /{id}/agent-install-cmd | install | server/api/servers.py:1897 | get_agent_install_cmd | service.get_server |
| POST | /{id}/upgrade-agent | ssh | server/api/servers.py:1965 | upgrade_agent | service.get_server |
| POST | /{id}/agent-diagnose | ssh | server/api/servers.py:2067 | agent_diagnose | service.get_server |
| POST | /check | ssh | server/api/servers.py:2105 | check_servers | |
| GET | /ip-allowlist | login,script | server/api/settings.py:168 | get_ip_allowlist | |
| GET | /bing-wallpaper | file | server/api/settings.py:307 | bing_wallpaper | |
| PUT | /{id} | script | server/api/settings.py:476 | update_schedule | |
| GET | / | ssh | server/api/settings.py:684 | list_ssh_key_presets | |
| POST | / | ssh | server/api/settings.py:702 | create_ssh_key_preset | |
| PUT | /{id} | ssh | server/api/settings.py:742 | update_ssh_key_preset | |
| POST | /{id}/reveal | ssh | server/api/settings.py:795 | reveal_ssh_key_preset | |
| DELETE | /{id} | ssh | server/api/settings.py:828 | delete_ssh_key_preset | |
| GET | / | script | server/api/settings.py:856 | list_alert_history | |
| GET | /ops-patrol/status | install | server/api/settings.py:1035 | ops_patrol_status | |
| POST | /ops-patrol/sync-telegram | file | server/api/settings.py:1056 | ops_patrol_sync_telegram | |
| POST | /ip-allowlist/parse-subscription | script | server/api/settings.py:1398 | parse_subscription | |
| POST | /ip-allowlist | login,script | server/api/settings.py:1503 | set_ip_allowlist | |
| POST | /ip-allowlist/toggle | login | server/api/settings.py:1569 | toggle_ip_allowlist | |
| POST | /ip-allowlist/manual | login | server/api/settings.py:1596 | add_manual_ips | |
| DELETE | /ip-allowlist/ip | login | server/api/settings.py:1632 | remove_allowlist_ip | |
| POST | /files | file | server/api/sync_v2.py:80 | sync_files | |
| POST | /preview | file | server/api/sync_v2.py:128 | preview_sync | |
| POST | /file-ops | file,ssh | server/api/sync_v2.py:161 | file_operation | |
| POST | /file-clipboard | file,ssh | server/api/sync_v2.py:228 | apply_file_clipboard | |
| POST | /browse | file,ssh | server/api/sync_v2.py:291 | browse_directory | |
| POST | /browse-local | file | server/api/sync_v2.py:322 | browse_local_directory | |
| POST | /local-file-ops | file | server/api/sync_v2.py:361 | local_file_operation | |
| POST | /local-file-preview | file | server/api/sync_v2.py:432 | local_file_preview | |
## 5. 典型调用链样例
| API | 入口 | 标签 | 调用链候选 |
| --- | --- | --- | --- |
| POST /script-callback | server/api/agent.py:245 | script | 未匹配到 service_calls |
| GET /nodes | server/api/assets.py:126 | script | 未匹配到 service_calls |
| GET /ssh-sessions | server/api/assets.py:230 | script,ssh | 未匹配到 service_calls |
| GET /command-logs | server/api/assets.py:282 | script,ssh | 未匹配到 service_calls |
| GET /login-access | server/api/auth.py:116 | login | 未匹配到 service_calls |
| POST /login | server/api/auth.py:128 | login | AuthService.login(DB×7;Redis×15;SSH×2) |
| PUT /password | server/api/auth.py:303 | login | AuthService.__init__(DB×7;Redis×15;SSH×2) -> AuthService.refresh_token(DB×7;Redis×15;SSH×2) -> AuthService._verify_totp(DB×7;Redis×15;SSH×2) |
| POST /webssh-token | server/api/auth.py:363 | ssh | AuthService.create_webssh_token(DB×7;Redis×15;SSH×2) |
| GET /me | server/api/auth.py:385 | login | 未匹配到 service_calls |
| GET /servers | server/api/btpanel.py:60 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/config | server/api/btpanel.py:68 | script,ssh,btpanel,install | 未匹配到 service_calls |
| PUT /servers/{server_id}/config | server/api/btpanel.py:81 | btpanel | 未匹配到 service_calls |
| GET /settings | server/api/btpanel.py:103 | btpanel | 未匹配到 service_calls |
| PUT /settings | server/api/btpanel.py:111 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/bootstrap | server/api/btpanel.py:129 | btpanel | 未匹配到 service_calls |
| POST /servers/batch-bootstrap | server/api/btpanel.py:148 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/login-url | server/api/btpanel.py:166 | login,btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/system/total | server/api/btpanel.py:183 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/system/disk | server/api/btpanel.py:191 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/system/network | server/api/btpanel.py:199 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/sites | server/api/btpanel.py:207 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/sites/start | server/api/btpanel.py:215 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/sites/stop | server/api/btpanel.py:228 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/sites/php-versions | server/api/btpanel.py:241 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/sites | server/api/btpanel.py:249 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/sites/{site_id}/domains | server/api/btpanel.py:280 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/domains | server/api/btpanel.py:293 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/domains/delete | server/api/btpanel.py:306 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/ssl/sites | server/api/btpanel.py:319 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/ssl/set | server/api/btpanel.py:327 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/ssl/apply | server/api/btpanel.py:345 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/databases | server/api/btpanel.py:358 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/databases | server/api/btpanel.py:366 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/databases/password | server/api/btpanel.py:384 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/databases/backup | server/api/btpanel.py:397 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/crontab | server/api/btpanel.py:410 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/services | server/api/btpanel.py:418 | btpanel | BtPanelService.service_admin(DB×7;Redis×16;SSH×7) |
| GET '' | server/api/execution_records.py:20 | script | 未匹配到 service_calls |
| GET /{record_id} | server/api/execution_records.py:41 | script | 未匹配到 service_calls |
| GET /browse | server/api/files.py:40 | file,ssh | 未匹配到 service_calls |
| GET /connection-check | server/api/install.py:1113 | file,install | 未匹配到 service_calls |
| GET /state | server/api/install.py:1469 | file,install | 未匹配到 service_calls |
| GET / | server/api/scripts.py:31 | script | ScriptService.list_scripts(DB×9;Redis×13;SSH×9) |
| GET /exec-config | server/api/scripts.py:42 | script | 未匹配到 service_calls |
| GET /credentials | server/api/scripts.py:56 | script | ScriptService.list_credentials(DB×9;Redis×13;SSH×9) |
| POST /credentials | server/api/scripts.py:66 | script | ScriptService.create_credential(DB×9;Redis×13;SSH×9) |
| PUT /credentials/{id} | server/api/scripts.py:90 | script | 未匹配到 service_calls |
| DELETE /credentials/{id} | server/api/scripts.py:131 | script | ScriptService.delete_credential(DB×9;Redis×13;SSH×9) |
| GET /executions | server/api/scripts.py:152 | script | ScriptService.list_executions(DB×9;Redis×13;SSH×9) |
| POST /executions/{id}/stop | server/api/scripts.py:167 | script | ScriptService.stop_execution(DB×9;Redis×13;SSH×9) -> ScriptService.get_execution(DB×9;Redis×13;SSH×9) |
| POST /executions/{id}/mark-stuck | server/api/scripts.py:181 | script | ScriptService.mark_execution_stuck(DB×9;Redis×13;SSH×9) -> ScriptService.get_execution(DB×9;Redis×13;SSH×9) |
| POST /executions/{id}/retry | server/api/scripts.py:198 | script | ScriptService._parse_execution_meta(DB×9;Redis×13;SSH×9) -> get_execution_record_detail(Redis×6) -> ScriptService.get_execution(DB×9;Redis×13;SSH×9) -> ScriptService.retry_executio... |
| GET /executions/{id} | server/api/scripts.py:238 | script | ScriptService.get_execution(DB×9;Redis×13;SSH×9) |
| GET /{id} | server/api/scripts.py:258 | script | ScriptService.get_script(DB×9;Redis×13;SSH×9) |
| POST / | server/api/scripts.py:271 | script | ScriptService.create_script(DB×9;Redis×13;SSH×9) |
| PUT /{id} | server/api/scripts.py:295 | script | ScriptService.get_script(DB×9;Redis×13;SSH×9) -> ScriptService.update_script(DB×9;Redis×13;SSH×9) |
| DELETE /{id} | server/api/scripts.py:325 | script | ScriptService.get_script(DB×9;Redis×13;SSH×9) -> ScriptService.delete_script(DB×9;Redis×13;SSH×9) |
| POST /exec | server/api/scripts.py:364 | script | ScriptService.get_execution(DB×9;Redis×13;SSH×9) -> ScriptService.execute_command(DB×9;Redis×13;SSH×9) |
| GET / | server/api/search.py:32 | script | 未匹配到 service_calls |
| GET / | server/api/servers.py:94 | script | 未匹配到 service_calls |
| GET /terminal-search-history | server/api/servers.py:436 | terminal | 未匹配到 service_calls |
| PUT /terminal-search-history | server/api/servers.py:454 | terminal | 未匹配到 service_calls |
| GET /logs | server/api/servers.py:491 | script | 未匹配到 service_calls |
| GET /import/template | server/api/servers.py:570 | file | 未匹配到 service_calls |
| POST /import | server/api/servers.py:589 | file | ServerService.create_server(DB×2;Redis×3;SSH×2) |
| POST /batch/detect-path | server/api/servers.py:850 | ssh | 未匹配到 service_calls |
| GET /pending | server/api/servers.py:977 | script,ssh | 未匹配到 service_calls |
| POST /add-by-ip | server/api/servers.py:1099 | login,ssh | 未匹配到 service_calls |
| GET /{id}/files-capability | server/api/servers.py:1407 | file,ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST /{id}/setup-files-sudo | server/api/servers.py:1422 | file,ssh,install | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST / | server/api/servers.py:1463 | file,ssh | ServerService.create_server(DB×2;Redis×3;SSH×2) |
| PUT /{id} | server/api/servers.py:1537 | file,ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) -> ServerService.update_server(DB×2;Redis×3;SSH×2) |
| POST /{id}/install-agent | server/api/servers.py:1711 | ssh,install | BtPanelService.get_server(DB×7;Redis×16;SSH×7) -> ServerService.update_server(DB×2;Redis×3;SSH×2) |
| POST /{id}/uninstall-agent | server/api/servers.py:1818 | file,ssh,install | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST /{id}/upgrade-agent | server/api/servers.py:1965 | ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST /{id}/agent-diagnose | server/api/servers.py:2067 | ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST /check | server/api/servers.py:2105 | ssh | 未匹配到 service_calls |
| GET /ip-allowlist | server/api/settings.py:168 | login,script | 未匹配到 service_calls |
| GET /bing-wallpaper | server/api/settings.py:307 | file | 未匹配到 service_calls |
| PUT /{id} | server/api/settings.py:476 | script | 未匹配到 service_calls |
## 6. 宝塔相关流
| 类型 | 位置 | 函数 | 标签/Key | 摘要 |
| --- | --- | --- | --- | --- |
| api | server/api/btpanel.py:60 | list_bt_servers | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_server_statuses', '_svc'], 'fi |
| api | server/api/btpanel.py:68 | get_bt_config | btpanel,install,script,ssh | {'auth_required_inferred': True, 'calls': ['router.get', 'Query', 'Depends', 'Depends', '_svc.get_config', '_http_error' |
| api | server/api/btpanel.py:81 | update_bt_config | btpanel | {'auth_required_inferred': True, 'calls': ['router.put', 'Depends', 'Depends', '_svc.update_config', '_http_error', '_sv |
| api | server/api/btpanel.py:103 | get_bt_global_settings | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.get_global_settings', '_svc'], 'fil |
| api | server/api/btpanel.py:111 | update_bt_global_settings | btpanel | {'auth_required_inferred': True, 'calls': ['router.put', 'Depends', 'Depends', '_svc.update_global_settings', '_http_err |
| api | server/api/btpanel.py:129 | bootstrap_bt_server | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.bootstrap_server', '_http_error', |
| api | server/api/btpanel.py:148 | batch_bootstrap_bt_servers | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.batch_bootstrap', '_http_error', ' |
| api | server/api/btpanel.py:166 | create_bt_login_url | btpanel,login | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.create_login_url', '_http_error', |
| api | server/api/btpanel.py:183 | bt_system_total | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.system_total', '_http_error', '_svc |
| api | server/api/btpanel.py:191 | bt_system_disk | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.system_disk', '_http_error', '_svc' |
| api | server/api/btpanel.py:199 | bt_system_network | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.system_network', '_http_error', '_s |
| api | server/api/btpanel.py:207 | bt_list_sites | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_sites', '_http_error', '_svc'] |
| api | server/api/btpanel.py:215 | bt_site_start | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.site_start', '_http_error', '_svc' |
| api | server/api/btpanel.py:228 | bt_site_stop | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.site_stop', '_http_error', '_svc'] |
| api | server/api/btpanel.py:241 | bt_php_versions | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.php_versions', '_http_error', '_svc |
| api | server/api/btpanel.py:249 | bt_create_site | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', 'json.dumps', '_svc.create_site', '_http |
| api | server/api/btpanel.py:280 | bt_list_domains | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_domains', '_http_error', '_svc |
| api | server/api/btpanel.py:293 | bt_add_domain | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.add_domain', '_http_error', 'body. |
| api | server/api/btpanel.py:306 | bt_delete_domain | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.delete_domain', '_http_error', 'bo |
| api | server/api/btpanel.py:319 | bt_ssl_sites | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_ssl_sites', '_http_error', '_s |
| api | server/api/btpanel.py:327 | bt_set_ssl | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.set_ssl', '_http_error', 'body.mod |
| api | server/api/btpanel.py:345 | bt_apply_ssl | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.apply_letsencrypt', '_http_error', |
| api | server/api/btpanel.py:358 | bt_list_databases | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_databases', '_http_error', '_s |
| api | server/api/btpanel.py:366 | bt_create_database | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.create_database', '_http_error', ' |
| api | server/api/btpanel.py:384 | bt_db_password | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.reset_database_password', '_http_e |
| api | server/api/btpanel.py:397 | bt_db_backup | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.backup_database', '_http_error', ' |
| api | server/api/btpanel.py:410 | bt_crontab | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_crontab', '_http_error', '_svc |
| api | server/api/btpanel.py:418 | bt_service_admin | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.service_admin', '_http_error', '_s |
| service | server/application/services/btpanel_bootstrap_schedule.py:24 | | btpanel | {'calls': ["getattr(settings, 'BT_PANEL_AUTO_BOOTSTRAP_ENABLED', None) or 'true'.strip.lower", "getattr(settings, 'BT_PA |
| service | server/application/services/btpanel_bootstrap_schedule.py:29 | | btpanel | {'calls': ['getattr', 'max', 'int', 'min'], 'class': None, 'end_line': 35, 'file': 'server/application/services/btpanel_ |
| service | server/application/services/btpanel_bootstrap_schedule.py:38 | | btpanel,session | {'calls': ['int', 'AsyncSessionLocal', 'ServerRepositoryImpl', 'mark_bootstrap_pending', 'repo.get_by_id', 'bt_panel_con |
| service | server/application/services/btpanel_bootstrap_schedule.py:54 | | btpanel | {'calls': ['is_auto_bootstrap_enabled', 'int', 'mark_servers_bootstrap_pending', 'asyncio.create_task', '_BG_TASKS.add', |
| service | server/application/services/btpanel_bootstrap_schedule.py:68 | | btpanel,session | {'calls': ['AsyncSessionLocal', 'BtPanelService', 'logger.warning', 'svc.bootstrap_server'], 'class': None, 'end_line': |
| service | server/application/services/btpanel_bootstrap_schedule.py:84 | | btpanel,session | {'calls': ['datetime.now', 'bootstrap_batch_size', 'is_auto_bootstrap_enabled', 'AsyncSessionLocal', 'ServerRepositoryIm |
| service | server/application/services/btpanel_service.py:54 | | btpanel | {'calls': ['isinstance'], 'class': None, 'end_line': 55, 'file': 'server/application/services/btpanel_service.py', 'id': |
| service | server/application/services/btpanel_service.py:59 | | btpanel | {'calls': ['ServerRepositoryImpl', 'AuditLogRepositoryImpl'], 'class': 'BtPanelService', 'end_line': 62, 'file': 'server |
| service | server/application/services/btpanel_service.py:64 | | btpanel | {'calls': ['self.servers.get_by_id', 'ValueError'], 'class': 'BtPanelService', 'end_line': 68, 'file': 'server/applicati |
| service | server/application/services/btpanel_service.py:70 | | btpanel | {'calls': ['read_bt_panel_credentials', 'BtPanelClient', 'ValueError'], 'class': 'BtPanelService', 'end_line': 74, 'file |
| service | server/application/services/btpanel_service.py:76 | | btpanel,redis | {'calls': ['get_redis', 'redis.pipeline', 'zip', 'self.servers.get_all', 'pipe.hgetall', 'pipe.execute', 'agent_is_insta |
| service | server/application/services/btpanel_service.py:112 | | btpanel | {'calls': ['self.get_server', 'self._resolve_bt_installed', 'public_bt_panel_status'], 'class': 'BtPanelService', 'end_l |
| service | server/application/services/btpanel_service.py:117 | | btpanel | {'calls': ['get_bt_panel_block', 'block.get', 'block.get', 'merge_bootstrap_fields', 'bt_panel_configured', 'detect_bt_p |
| service | server/application/services/btpanel_service.py:141 | | btpanel | {'calls': ["getattr(settings, 'BT_PANEL_SOURCE_IP', None) or ''.strip", 'self._auto_bootstrap_enabled', 'get_bt_panel_so |
| service | server/application/services/btpanel_service.py:150 | | btpanel | {'calls': ['bt_panel_source_ip.strip', 'self.audit.create', 'self.get_global_settings', 'ValueError', 'self._persist_set |
| service | server/application/services/btpanel_service.py:183 | | btpanel | {'calls': ['SettingRepositoryImpl', 'settings.apply_db_override', 'repo.set', 'publish_setting_change'], 'class': 'BtPan |
| service | server/application/services/btpanel_service.py:194 | | btpanel | {'calls': ['is_auto_bootstrap_enabled'], 'class': 'BtPanelService', 'end_line': 197, 'file': 'server/application/service |
| service | server/application/services/btpanel_service.py:199 | | btpanel | {'calls': ['merge_bt_panel_extra', 'self.get_server', 'self.servers.update', 'self.audit.create', 'self.get_config', 'is |
| service | server/application/services/btpanel_service.py:249 | | btpanel | {'calls': ['bootstrap_lock', 'self._bootstrap_server_locked'], 'class': 'BtPanelService', 'end_line': 265, 'file': 'serv |
| service | server/application/services/btpanel_service.py:267 | | btpanel | {'calls': ['get_bt_panel_source_ip', 'str.strip', 'str.strip', 'apply_bootstrap_success', 'self.get_server', 'bt_panel_c |
| service | server/application/services/btpanel_service.py:368 | | btpanel | {'calls': ['ValueError', 'start_batch_job', 'self.list_server_statuses', 'int', 'int', 'ids.append', 's.get'], 'class': |
| service | server/application/services/btpanel_service.py:398 | | btpanel | {'calls': ['self.audit.create', 'AuditLog', 'json.dumps'], 'class': 'BtPanelService', 'end_line': 421, 'file': 'server/a |
| service | server/application/services/btpanel_service.py:424 | | btpanel | {'calls': ['getattr', 'getattr', 'bool', "getattr(server, 'auth_method', None) or ''.strip", 'bool', 'getattr', 'getattr |
| service | server/application/services/btpanel_service.py:437 | | btpanel,session | {'calls': ['get_bt_panel_block', '_parse_iso', '_utcnow() - checked_at.total_seconds', 'block.get', 'block.get', '_utcno |
| service | server/application/services/btpanel_service.py:447 | | btpanel,session | {'calls': ['merge_bootstrap_fields', 'self.servers.update', 'self.get_server', 'isinstance', '_iso', '_utcnow'], 'class' |
| service | server/application/services/btpanel_service.py:457 | | btpanel,session | {'calls': ['get_bt_panel_source_ip', 'str.strip', 'str.strip', 'apply_bootstrap_success', 'self._session_cleanup_ttl_che |
| service | server/application/services/btpanel_service.py:502 | | btpanel | {'calls': ['login_url_lock', 'self._create_login_url_fresh', 'self._audit_login_url_failure'], 'class': 'BtPanelService' |
| service | server/application/services/btpanel_service.py:529 | | btpanel,session | {'calls': ['bt_panel_configured', 'read_bt_panel_credentials', 'self.get_server', 'bt_panel_configured', 'read_bt_panel_ |
| service | server/application/services/btpanel_service.py:585 | | btpanel | {'calls': ['self.audit.create', 'logger.warning', 'AuditLog', 'json.dumps'], 'class': 'BtPanelService', 'end_line': 607, |
| service | server/application/services/btpanel_service.py:609 | | btpanel | {'calls': ['self._repair_ip_whitelist', 'read_bt_panel_credentials', 'logger.warning', 'logger.warning', 'self.get_serve |
| service | server/application/services/btpanel_service.py:642 | | btpanel | {'calls': ['BtPanelClient', 'isinstance', 'BtPanelApiError', 'int', 'client.post', 'data.get', 'time.time', 'data.get', |
| service | server/application/services/btpanel_service.py:674 | | btpanel | {'calls': ['self._client', 'self.get_server', 'client.post', 'str', 'self.get_server', 'self._client.post', 'self._repai |
| service | server/application/services/btpanel_service.py:692 | | btpanel | {'calls': ['get_bt_panel_source_ip', 'str.strip', 'str.strip', 'logger.info', 'logger.warning', 'apply_bootstrap_success |
| service | server/application/services/btpanel_service.py:722 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 723, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:725 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 726, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:728 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 729, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:732 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 738, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:740 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 741, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:743 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 744, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:747 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 748, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:750 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps', 'payload.get'], 'class': 'BtPanelService', 'end_ |
| service | server/application/services/btpanel_service.py:763 | | btpanel | {'calls': ['self.proxy', 'str'], 'class': 'BtPanelService', 'end_line': 768, 'file': 'server/application/services/btpane |
| service | server/application/services/btpanel_service.py:770 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 771, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:773 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 774, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:777 | | btpanel | {'calls': ['self.list_sites'], 'class': 'BtPanelService', 'end_line': 778, 'file': 'server/application/services/btpanel_ |
| service | server/application/services/btpanel_service.py:780 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps', 'payload.get'], 'class': 'BtPanelService', 'end_ |
| service | server/application/services/btpanel_service.py:792 | | btpanel,crypto | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 793, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:796 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 801, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:803 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps', 'payload.get'], 'class': 'BtPanelService', 'end_ |
| service | server/application/services/btpanel_service.py:815 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 816, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:818 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 819, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:822 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 823, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:826 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps'], 'class': 'BtPanelService', 'end_line': 836, 'fi |
| service | server/application/services/server_batch_service.py:774 | | btpanel,session | {'calls': ['str', 'server_map.get', '_run_with_semaphore', 'live.get', 'result_item_dict', 'result.get', 'result.get', ' |
| service | server/application/services/server_batch_service.py:779 | | btpanel,session | {'calls': ['server_map.get', 'result_item_dict', 'result.get', 'result.get', 'result_item_dict', 'AsyncSessionLocal', 'B |
| service | server/application/services/server_file_transfer_service.py:397 | | btpanel,token | {'calls': ['read_bt_panel_credentials', 'signed_download_url', 'ServerTransferError', 'ServerTransferError', 'normalize_ |
| service | server/application/services/server_file_transfer_service.py:497 | | btpanel | {'calls': ['_assert_distinct_servers', 'int', '_get_server', 'bt_panel_configured', 'read_bt_panel_credentials', 'create |
| redis | tests/test_btpanel_client.py:39 | test_merge_bt_panel_extra_encrypts_key | enc:secret,enc:{s} | {'file': 'tests/test_btpanel_client.py', 'function': 'test_merge_bt_panel_extra_encrypts_key', 'id': 'tests/test_btpanel |
| redis | tests/test_btpanel_get_config.py:64 | test_bootstrap_lock_serializes_same_server | | {'file': 'tests/test_btpanel_get_config.py', 'function': 'test_bootstrap_lock_serializes_same_server', 'id': 'tests/test |
| redis | tests/test_btpanel_get_config.py:67 | test_bootstrap_lock_serializes_same_server.worker | | {'file': 'tests/test_btpanel_get_config.py', 'function': 'test_bootstrap_lock_serializes_same_server.worker', 'id': 'tes |
| redis | tests/test_btpanel_login_url.py:19 | _FakeRedis.get | | {'file': 'tests/test_btpanel_login_url.py', 'function': '_FakeRedis.get', 'id': 'tests/test_btpanel_login_url.py::_FakeR |
| redis | tests/test_btpanel_login_url.py:27 | _noop_login_url_lock | | {'file': 'tests/test_btpanel_login_url.py', 'function': '_noop_login_url_lock', 'id': 'tests/test_btpanel_login_url.py:: |
| redis | tests/test_btpanel_login_url.py:32 | test_create_login_url_bootstraps_when_unconfigured | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_bootstraps_when_unconfigured', 'id': 'tes |
| redis | tests/test_btpanel_login_url.py:78 | test_create_login_url_does_not_reuse_single_use_tokens | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_does_not_reuse_single_use_tokens', 'id': |
| redis | tests/test_btpanel_login_url.py:119 | test_create_login_url_skips_bootstrap_when_configured | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_skips_bootstrap_when_configured', 'id': ' |
| redis | tests/test_btpanel_login_url.py:161 | test_create_login_url_ensures_session_cleanup_ttl_once_for_configured_server | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_ensures_session_cleanup_ttl_once_for_conf |
| redis | tests/test_btpanel_login_url.py:224 | test_create_login_url_repairs_stale_port_then_uses_api | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_repairs_stale_port_then_uses_api', 'id': |
| redis | tests/test_btpanel_login_url.py:288 | test_create_login_url_falls_back_to_ssh_when_api_unreachable | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_falls_back_to_ssh_when_api_unreachable', |
| redis | tests/test_btpanel_server_list.py:23 | test_list_server_statuses_includes_live_online_from_redis | | {'file': 'tests/test_btpanel_server_list.py', 'function': 'test_list_server_statuses_includes_live_online_from_redis', ' |
| redis | tests/test_btpanel_ssh_bootstrap.py:34 | test_mark_bootstrap_pending_sets_now | | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_mark_bootstrap_pending_sets_now', 'id': 'tests/test_bt |
| redis | tests/test_btpanel_ssh_bootstrap.py:42 | test_apply_failure_bt_not_installed_goes_installing | | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_apply_failure_bt_not_installed_goes_installing', 'id': |
| redis | tests/test_btpanel_ssh_bootstrap.py:50 | test_apply_failure_ssh_auth_is_permanent | | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_apply_failure_ssh_auth_is_permanent', 'id': 'tests/tes |
| redis | tests/test_btpanel_ssh_bootstrap.py:57 | test_apply_success_sets_ready | enc:token123,enc:{s} | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_apply_success_sets_ready', 'id': 'tests/test_btpanel_s |
| redis | server/api/btpanel.py:46 | _http_error | | {'file': 'server/api/btpanel.py', 'function': '_http_error', 'id': 'server/api/btpanel.py::_http_error::redis', 'key_pat |
| redis | server/infrastructure/btpanel/bootstrap_lock.py:13 | bootstrap_lock | | {'file': 'server/infrastructure/btpanel/bootstrap_lock.py', 'function': 'bootstrap_lock', 'id': 'server/infrastructure/b |
| redis | server/infrastructure/btpanel/bootstrap_state.py:43 | get_bootstrap_block | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'get_bootstrap_block', 'id': 'server/infrastruc |
| redis | server/infrastructure/btpanel/bootstrap_state.py:78 | public_bootstrap_fields | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'public_bootstrap_fields', 'id': 'server/infras |
| redis | server/infrastructure/btpanel/bootstrap_state.py:96 | merge_bootstrap_fields | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'merge_bootstrap_fields', 'id': 'server/infrast |
| redis | server/infrastructure/btpanel/bootstrap_state.py:126 | bt_panel_configured_from_attrs | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'bt_panel_configured_from_attrs', 'id': 'server |
| redis | server/infrastructure/btpanel/bootstrap_state.py:135 | is_eligible_for_background_bootstrap | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'is_eligible_for_background_bootstrap', 'id': ' |
| redis | server/infrastructure/btpanel/bootstrap_state.py:155 | apply_bootstrap_success | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'apply_bootstrap_success', 'id': 'server/infras |
| redis | server/infrastructure/btpanel/bootstrap_state.py:196 | apply_bootstrap_failure | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'apply_bootstrap_failure', 'id': 'server/infras |
| redis | server/infrastructure/btpanel/bootstrap_state.py:223 | get_bootstrap_block_from_attrs | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'get_bootstrap_block_from_attrs', 'id': 'server |
| redis | server/infrastructure/btpanel/client.py:29 | raise_if_panel_error | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'raise_if_panel_error', 'id': 'server/infrastructure/btp |
| redis | server/infrastructure/btpanel/client.py:50 | BtPanelClient._load_cookies | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'BtPanelClient._load_cookies', 'id': 'server/infrastruct |
| redis | server/infrastructure/btpanel/client.py:60 | BtPanelClient._save_cookies | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'BtPanelClient._save_cookies', 'id': 'server/infrastruct |
| redis | server/infrastructure/btpanel/client.py:64 | BtPanelClient.post | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'BtPanelClient.post', 'id': 'server/infrastructure/btpan |
| redis | server/infrastructure/btpanel/credentials.py:26 | get_bt_panel_block | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'get_bt_panel_block', 'id': 'server/infrastructure/ |
| redis | server/infrastructure/btpanel/credentials.py:31 | bt_panel_configured | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'bt_panel_configured', 'id': 'server/infrastructure |
| redis | server/infrastructure/btpanel/credentials.py:36 | get_bt_panel_base_url | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'get_bt_panel_base_url', 'id': 'server/infrastructu |
| redis | server/infrastructure/btpanel/credentials.py:41 | read_bt_panel_credentials | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'read_bt_panel_credentials', 'id': 'server/infrastr |
| redis | server/infrastructure/btpanel/credentials.py:54 | merge_bt_panel_extra | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'merge_bt_panel_extra', 'id': 'server/infrastructur |
## 7. 安全审查优先级建议
1. **宝塔一键登录/session**:优先审查 `server/application/services/btpanel_service.py``server/infrastructure/btpanel/*`,关注 token TTL、Redis lock、并发打开 10 个面板、失败回退和审计日志。
2. **文件管理/归档/解压**:优先审查带 `file` 标签的 API 与 `server/infrastructure/ssh/remote_archive.py`、文件传输路径,关注路径穿越、tar/zip 选项注入、软链、绝对路径。
3. **SSH/远程 shell**:用户确认管理员远程执行 shell 是业务能力,不作为漏洞本身;审查重点应放在鉴权、审计、超时、输出截断、日志脱敏、命令模板边界。
4. **数据库访问**:对 `db_queries.jsonl` 中写操作链路检查权限过滤、事务 commit/rollback、分页上限和多租户隔离。
5. **Redis 锁/缓存**:对 login/session/任务类 Redis key 检查 TTL、锁释放、失败重试与缓存穿透。
@@ -0,0 +1,349 @@
# Nexus API 限速与并发限制盘点
日期:2026-07-07
范围:本地代码上下文快照 `work/nexus_code_context/snapshots/nexus-source-20260707-112042`
说明:本报告只盘点代码内的 API rate limit、429、throttle、并发/批量限制;不包含外部平台(例如 Codex 模型 API)的 429。
## 结论
1. 当前 Nexus 后端没有发现全局 HTTP API 限速中间件。
- 未发现 slowapi / fastapi-limiter / starlette limiter 等依赖或全局 middleware。
- `server/main.py` 主要是安装模式、DB session、安全头、AppAuth、JWT、CORS 等中间件,没有全局 rate limit。
2. 当前会返回 HTTP 429 的后端代码只有两类:
- 管理员登录失败锁定:`/api/auth/login`
- 子机脚本回调限速:`/api/agent/script-callback`
3. 大量“速度限制”其实不是 API rate limit,而是远程 SSH / 同步 / 批量任务的并发保护。
- 这些限制用于避免同时打爆子机、SSH、MySQL、Redis 或 Nexus 自身 worker。
- 用户正常浏览后台 API 不会被这些限制直接 429。
## 1. 全局 API 限速
### 当前状态
没有发现全局限速。
检索点:
- `rate_limit` / `ratelimit` / `throttle` / `slowapi` / `limiter`
- `status_code=429`
- FastAPI middleware
- Docker / Uvicorn worker 配置
### 影响
- 好处:后台操作不会因为普通访问频率触发全局 429。
- 风险:如果公网暴露,普通 API 缺少统一抗刷保护;目前主要依赖登录锁定、鉴权、业务级并发控制。
## 2. 登录接口限速 / 锁定
### 位置
- `server/api/auth.py:127-156`
- `server/application/services/auth_service.py:37-39`
- `server/application/services/auth_service.py:99-107`
- `server/infrastructure/database/admin_repo.py:41-53`
### 规则
```text
MAX_LOGIN_FAILURES = 5
LOCKOUT_MINUTES = 15
```
逻辑:
```text
/api/auth/login
-> AuthService.login()
-> count_recent_failures(username, ip_address, 15)
-> 最近 15 分钟同 username 失败次数 >= 5
-> 返回 reason=account_locked
-> API 层转成 HTTP 429
```
仓库实现实际按 username 统计最近失败次数:
```text
LoginAttempt.username == username
LoginAttempt.success == False
LoginAttempt.attempted_at >= cutoff
```
备注:白名单 IP 会跳过失败计数锁定。
### 影响
这是登录防爆破,不是普通 API 限速。
如果登录时出现 429,含义是账号登录失败过多锁定 15 分钟。
## 3. 脚本回调接口限速
### 位置
- `server/api/agent.py:244-264`
- `server/infrastructure/redis/script_callback_rate.py:14-16`
- `server/infrastructure/redis/script_callback_rate.py:31-44`
### 规则
```text
每 job_id10 次 / 60 秒
每 IP60 次 / 60 秒
```
Redis key
```text
script_cb:job:{job_id}
script_cb:ip:{ip}
```
触发后返回:
```text
HTTP 429 Too many script callback requests
```
### 影响
这是专门保护子机长任务脚本回调的,避免未登录回调接口被刷或同一 job 重复回调过多。
不会影响后台普通 API,也不会影响宝塔一键登录。
## 4. 脚本远程执行并发限制
### 位置
- `server/config.py:66-69`
- `server/api/scripts.py:41-50`
- `server/application/services/script_service.py:254-260`
- `server/application/services/script_service.py:759-761`
### 当前值
```text
SCRIPT_EXEC_BATCH_SIZE = 50
SCRIPT_EXEC_CONCURRENCY = 10
```
含义:
- 一批最多 50 台服务器。
- 每批同时最多 10 个 SSH 执行。
- `/api/scripts/exec-config` 会返回这些值给前端显示。
### 影响
这是远程命令执行的并发保护,不是 HTTP API 限速。
如果一次选很多服务器,会按批次和并发执行,所以看起来“执行速度被限制”。
用户已说明 `script_service` 是管理员远程执行命令功能,任意 shell 本身不是问题;这里仅记录并发限制。
## 5. 同步/推送并发限制
### 位置
- `server/api/schemas.py:224-225`
- `server/application/services/sync_engine_v2.py:39`
- `server/application/services/sync_engine_v2.py:113-127`
### 当前值
API schema
```text
batch_size: 默认 50,范围 1-200
concurrency: 默认 10,范围 1-50
```
服务层硬上限:
```text
MAX_CONCURRENT = 10
concurrency = min(concurrency, MAX_CONCURRENT)
```
### 影响
虽然 API 入参允许 concurrency 到 50,但服务层最终会压到最多 10。
所以同步/推送如果前端或 API 传 50,实际仍最多 10 并发。
这是一个“配置表现不一致”的点。
## 6. 同步校验 verify 并发限制
### 位置
- `server/api/sync_v2.py:1352-1354`
### 当前值
```text
sem = asyncio.Semaphore(5)
```
### 影响
同步校验会最多 5 台并发 SSH 校验。
这也不是 API 429,而是校验任务执行速度保护。
## 7. 批量服务器操作并发限制
### 位置
- `server/application/services/server_batch_service.py:35`
- `server/application/services/server_batch_service.py:427-446`
### 当前值
```text
CONCURRENCY = 5
```
适用操作包括:
- health-check
- detect-path
- detect-domain
- onboard
- install-agent
- upgrade-agent
- uninstall-agent
- bt-panel-bootstrap
### 影响
批量服务器操作最多 5 台并发。
宝塔批量 bootstrap 也走这里。
这不是 HTTP 限速,而是远程任务并发限制。
## 8. SSH 健康检查并发限制
### 位置
- `server/application/server_connectivity.py:124-142`
### 当前值
```text
concurrency = 10
sem = asyncio.Semaphore(max(1, concurrency))
```
### 影响
用于批量 SSH health check,默认 10 并发。
## 9. WebSocket 连接数限制
### 位置
- `server/api/websocket.py:42-53`
### 当前值
```text
MAX_CONNECTIONS = 500
```
超过后关闭连接:
```text
code=4003, reason="Too many connections"
```
### 影响
这不是 HTTP API 限速,是单 worker 内存里的 WebSocket 连接数上限。
## 10. 前端侧并发/节流
### 位置
- `frontend/src/api/index.ts`:只处理后端 429 文案,没有主动限速。
- `frontend/src/utils/filePreload.ts:12`:文件预加载并发 4。
- `frontend/src/composables/push/types.ts:98`:推送预览并发 4。
- `frontend/src/composables/push/usePushPreview.ts:75-76`:按 4 个一批预览。
### 影响
前端没有全局 API throttle/debounce。
只有局部预加载/预览并发保护。
## 11. 部署侧限制
### 位置
- `Dockerfile:43`
当前容器命令:
```text
uvicorn server.main:app --host 0.0.0.0 --port 8600
```
没有发现:
- gunicorn worker 数配置
- uvicorn --workers 配置
- nginx limit_req 配置
- compose 侧 API rate limit
### 影响
当前 Docker 部署默认单 Uvicorn worker。
这不是“限速”,但会影响并发吞吐;远程 SSH 操作如果阻塞资源,也可能让 API 响应变慢。
## 12. 如果要调整,先给 6 个方向
后续如果要改,我建议先选方向,不直接动代码:
### 方案 A:不加全局限速,只调远程任务并发
- 调整脚本执行、同步、批量任务并发。
- 适合内网/自用后台。
- 风险低,不影响普通 API。
### 方案 B:加登录/敏感接口更严格限速
- 保持普通 API 不限速。
- 对登录、令牌刷新、敏感设置、脚本回调加 Redis 限速。
- 安全收益高,误伤较少。
### 方案 C:加全局 Redis 限速,但白名单内网/IP
- 对所有 API 做每 IP / 每用户限速。
- 内网或管理员 IP 放宽。
- 更安全,但可能误伤批量操作和前端轮询。
### 方案 D:按接口分层限速
- 读接口宽松。
- 写接口中等。
- 登录/敏感/远程执行严格。
- 最合理,但实现和测试工作量最大。
### 方案 E:只优化吞吐,不做 rate limit
- 增加 uvicorn/gunicorn workers。
- 调 DB pool、Redis、SSH 线程池/并发。
- 目标是更快,不是防刷。
### 方案 F:把后台远程任务全部队列化
- API 只创建任务,worker 队列执行。
- 支持取消、重试、并发池、优先级。
- 长期最稳,但架构改动最大。
## 当前建议
如果你的目标是“后台 API 不要被限速”,当前代码已经没有全局 API 限速。
真正影响速度的主要是:
1. 同步服务硬上限 `MAX_CONCURRENT = 10`
2. 批量服务器操作 `CONCURRENCY = 5`
3. 脚本执行 `SCRIPT_EXEC_CONCURRENCY = 10``SCRIPT_EXEC_BATCH_SIZE = 50`
4. Docker 默认单 Uvicorn worker。
如果你的目标是“提高批量操作速度”,优先讨论方案 A 或 E。
如果你的目标是“增强公网安全”,优先讨论方案 B 或 D。
@@ -0,0 +1,175 @@
# Nexus SSH/文件管理安全巡检阶段 12026-07-07
## 1. 巡检口径调整
用户已明确:`script_service` 是“管理员远程执行命令”功能,天然允许管理员执行任意 shell;这是产品设计内能力,不作为本轮漏洞修复项。
后续对它只保留三类审查:
- 鉴权:必须只能由已登录管理员调用。
- 审计:执行人、目标服务器、执行时间、结果摘要要可追踪。
- 敏感信息:不要把密码、Token、密钥等完整写入日志或返回给不该看到的人。
本轮实际修复的是“非脚本服务、非预期的命令参数边界”。
---
## 2. 当前测试机状态
- 测试机:`192.168.124.219`
- 项目目录:`/opt/nexus-dev-current`
- Git 分支:`audit/security-review`
- 当前最新提交:`bcec78f fix: harden file manager shell operations`
- Nexus 容器:`0.0.0.0:18600 -> 8600`,健康检查返回 `ok`
- MySQL`0.0.0.0:13306 -> 3306`
- Redis`0.0.0.0:16379 -> 6379`
最近提交:
```text
bcec78f fix: harden file manager shell operations
112e617 fix: stop archive option injection via filenames
99edd30 chore: default compose ports for nexus test deploy
a538358 fix: keep bt panel sessions alive after one-click login
9d42fcd chore: baseline Nexus LAN test deployment snapshot
```
---
## 3. 已完成修复 A:远程压缩命令参数终止符
提交:`112e617 fix: stop archive option injection via filenames`
修改文件:
- `server/infrastructure/ssh/remote_archive.py`
- `tests/test_remote_archive_commands.py`
修复点:
- `tar` 压缩命令增加 `--`,避免成员名以 `-` 开头时被当作 tar 参数。
- `zip` 压缩命令增加 `--`,避免目标名或成员名以 `-` 开头时被当作 zip 参数。
运行容器已热更新确认包含:
```text
tar -czf ... -C ... -- ...
zip -qr -- ...
```
---
## 4. 已完成修复 B:文件管理命令参数终止符
提交:`bcec78f fix: harden file manager shell operations`
修改文件:
- `server/api/sync_v2.py`
加固命令:
```text
rm -rf -- <path>
mv -- <src> <dest>
mkdir -p -- <path>
cp -r -- <sources> <dest>
mv -t <dest> -- <sources>
```
说明:
- 这些路径本身已经经过 `normalize_remote_abs_path()``shlex.quote()`
- 本次增加 `--` 是第二道防线,防止未来路径来源变化或边界输入导致工具把路径误判为命令选项。
- 这不是限制管理员能力;管理员确实要执行任意命令时仍应通过设计内的 `script_service`
---
## 5. 已完成修复 C:递归 chmod/chown 系统目录子树保护
提交:`bcec78f fix: harden file manager shell operations`
修改文件:
- `server/utils/files_chmod_policy.py`
- `tests/test_file_permissions.py`
修复前:
- 只禁止对精确系统路径递归改权限,例如 `/etc``/usr``/`
-`/etc/nginx``/usr/local` 这类系统目录子树仍可能被文件管理 UI 执行递归 chmod/chown。
修复后:
- 继续禁止精确系统路径。
- 新增禁止关键系统目录子树:
- `/bin/`
- `/boot/`
- `/dev/`
- `/etc/`
- `/lib/`
- `/lib64/`
- `/proc/`
- `/run/`
- `/sbin/`
- `/sys/`
- `/usr/`
- `/var/cache/`
- `/var/lib/`
- `/var/log/`
- `/var/run/`
- `/var/spool/`
- 保留常见 Web 目录可操作:
- `/www/wwwroot/site`
- `/var/www/html`
设计原因:
- 文件管理 UI 的 chmod/chown 是“安全操作封装”,应防止误点造成系统不可用。
- 真正需要改系统目录权限的管理员,仍可使用设计内的 `script_service` 明确执行命令。
---
## 6. 验证结果
已运行测试:
```text
pytest tests/test_file_permissions.py tests/test_schema_path_validators.py tests/test_remote_archive_commands.py -q
22 passed in 0.54s
```
完整后端测试:
```text
pytest -q
733 passed, 1 skipped in 11.77s
```
格式检查:
```text
git diff --check
无输出,表示无 whitespace 错误。
```
运行容器热更新:
- 已把以下文件复制进 `nexus-nexus-1` 容器:
- `server/infrastructure/ssh/remote_archive.py`
- `server/api/sync_v2.py`
- `server/utils/files_chmod_policy.py`
- 已重启 Nexus 容器。
- 健康检查返回 `ok`
---
## 7. 下一步继续巡检建议
下一阶段建议按以下顺序继续:
1. `server/api/sync_v2.py` 解压链路:检查 tar/zip 解压是否需要增加归档成员路径校验,防 zip-slip/tar traversal。
2. `server/application/services/server_file_transfer_service.py`:检查跨服务器打包/投递的路径校验、临时目录清理、权限回退。
3. `server/infrastructure/ssh/remote_shell.py``asyncssh_pool.py`:复核 timeout、sudo fallback、stderr/stdout 截断和敏感信息处理。
4. `server/infrastructure/btpanel/ssh_bootstrap.py``ssh_login.py`:复核宝塔 token、白名单、session patch 输出中是否有敏感信息泄漏。
5. `server/api/servers.py`:复核 agent 安装/升级命令拼接、参数来源、失败回滚边界。
@@ -0,0 +1,177 @@
# Nexus SSH/文件管理安全巡检阶段 2:安全解压(2026-07-07
## 1. 本阶段目标
继续 SSH/文件管理安全巡检,重点检查:
- `/api/sync/decompress` 解压链路。
- tar/zip 归档成员是否可能写出目标目录。
- 归档中的软链接、硬链接、设备文件是否可能造成覆盖或越权写入。
`script_service` 仍按用户确认的口径处理:它是设计内管理员远程 shell,不作为漏洞;本阶段只加固文件管理 UI 的“封装式安全操作”。
---
## 2. 修复提交
提交:
```text
845d7e2 fix: validate remote archives before extraction
```
修改文件:
```text
server/api/sync_v2.py
server/infrastructure/ssh/remote_archive.py
tests/test_remote_archive_commands.py
```
---
## 3. 修复前风险
原解压逻辑直接执行:
```bash
tar xzf <archive> -C <dest>
unzip -o <archive> -d <dest>
```
虽然 `<archive>``<dest>` 已经做过绝对路径规范化和 shell quote,但归档文件内部成员名不受 Nexus 控制,仍需要额外检查:
- 成员名为 `../evil` 可能尝试路径穿越。
- 成员名为 `/etc/passwd` 属于绝对路径。
- zip 中 Windows 分隔符 `dir\evil` 在某些工具/平台语义下容易产生歧义。
- 归档内软链接/硬链接/设备文件可能造成解压后写出目标目录或制造特殊文件。
---
## 4. 修复后设计
解压流程从 API 中下沉到:
```text
server/infrastructure/ssh/remote_archive.py::run_remote_decompress()
```
新流程:
1. 判断格式:
- `.tar.gz` / `.tgz` -> `tar.gz`
- `.zip` -> `zip`
2. 先列出成员名:
- tar`tar -tzf <archive>`
- zip`unzip -Z1 <archive>`
3. 校验成员名:
- 禁止空文件名。
- 禁止绝对路径。
- 禁止 `..` 路径穿越。
- 禁止 Windows 反斜杠分隔符。
4. 再列出成员类型:
- tar`tar -tvzf <archive>`
- zip`zipinfo -l <archive>`
5. 校验成员类型:
- 允许普通文件和目录。
- 禁止符号链接。
- 禁止硬链接。
- 禁止块设备、字符设备、FIFO、socket。
6. 通过校验后才解压。
安全 tar 解压命令增加:
```bash
tar --no-same-owner --no-same-permissions -xzf <archive> -C <dest>
```
避免归档强行恢复 owner/permission,降低解压后权限异常风险。
---
## 5. 新增测试覆盖
新增/扩展测试文件:
```text
tests/test_remote_archive_commands.py
```
覆盖:
- tar/zip 成员列表命令。
- tar 解压命令使用 `--no-same-owner --no-same-permissions`
- 拒绝 `../evil.txt`
- 拒绝 `/etc/passwd`
- 拒绝 `dir\evil.txt`
- 允许普通相对文件和目录。
- 拒绝符号链接。
- 拒绝字符设备。
- 允许 zipinfo header/footer + 普通文件/目录。
---
## 6. 验证结果
相关测试:
```text
pytest tests/test_remote_archive_commands.py tests/test_schema_path_validators.py tests/test_file_permissions.py -q
31 passed in 0.55s
```
完整后端测试:
```text
pytest -q
742 passed, 1 skipped in 11.83s
```
格式检查:
```text
git diff --check
无输出
```
运行容器热更新:
- 已复制以下文件到 `nexus-nexus-1`
- `server/api/sync_v2.py`
- `server/infrastructure/ssh/remote_archive.py`
- 已重启 Nexus 容器。
- `http://127.0.0.1:18600/health` 返回 `ok`
- 容器内已确认存在:
- `validate_archive_member_names`
- `run_remote_decompress`
- API 调用 `run_remote_decompress(...)`
---
## 7. 当前最新提交链
```text
845d7e2 fix: validate remote archives before extraction
bcec78f fix: harden file manager shell operations
112e617 fix: stop archive option injection via filenames
99edd30 chore: default compose ports for nexus test deploy
a538358 fix: keep bt panel sessions alive after one-click login
```
---
## 8. 下一阶段建议
继续巡检:
1. `server/application/services/server_file_transfer_service.py`
- 跨服务器打包/拉取/投递路径校验。
- 临时文件清理。
- sudo fallback 边界。
2. `server/infrastructure/ssh/remote_shell.py` / `asyncssh_pool.py`
- timeout 默认值。
- stdout/stderr 截断。
- 敏感信息是否可能返回前端。
3. `server/api/servers.py`
- agent 安装/升级/回滚命令拼接。
- 参数来源和 quote。
@@ -0,0 +1,87 @@
# Nexus SSH 命令执行安全巡检阶段 3:跨服务器文件传输与项目专用 Skill
日期:2026-07-07
分支:`audit/security-review`
范围:跨服务器文件传输、BT 面板直链下载投递、远程归档压缩/解压、项目专用审查 Skill。
## 1. 本阶段结论
本阶段补齐了跨服务器文件传输链路中遗留的高风险边界:
- 传输包在目标服务器解压时,复用了统一的安全解压流程,先列成员、校验路径和类型,再解压。
- 远程 `mkdir``mv``rm`、归档 staging move 增加 `--` 参数终止符,避免以 `-` 开头的路径被当成命令选项。
- `pull_transfer_package` 与 BT 直链投递目标路径改为统一 `normalize_remote_abs_path` 校验,拒绝 `..` 与反斜杠。
- 新增 `.skills/` 下 Nexus 专用安全巡检规范,后续每轮审查可按固定规则执行。
完整测试结果:`748 passed, 1 skipped`
## 2. 调用链
### 2.1 Nexus 代理下载投递
```mermaid
flowchart TD
A["API: transfer package/deliver"] --> B["server_file_transfer_service.create_transfer_package"]
B --> C["run_remote_compress(source)"]
C --> D["Redis transfer_package_store: token/meta/ttl"]
D --> E["pull_transfer_package(dest)"]
E --> F["curl Nexus signed transfer URL to /tmp staging"]
F --> G["mv -- staging -> dest archive path"]
G --> H["run_remote_decompress(dest): list members"]
H --> I["validate names/types"]
I --> J["safe tar/unzip extract"]
J --> K["apply_transfer_permissions"]
```
### 2.2 BT 面板直链下载投递
```mermaid
flowchart TD
A["deliver_transfer_package"] --> B["source has BT API credentials"]
B --> C["_deliver_via_btpanel_download"]
C --> D["run_remote_compress(source)"]
D --> E["signed_download_url(BT)"]
E --> F["dest curl BT download URL to /tmp staging"]
F --> G["mv -- staging -> dest archive path"]
G --> H["run_remote_decompress(dest)"]
H --> I["validate archive member names/types"]
I --> J["extract + permissions"]
```
## 3. 修复点
| 文件 | 修复 |
|---|---|
| `server/application/services/server_file_transfer_service.py` | `_extract_archive_on_server()` 改为调用 `run_remote_decompress()`,解压前校验归档成员;目标路径使用 `normalize_remote_abs_path()``mkdir/mv/rm` 增加 `--`。 |
| `server/infrastructure/ssh/remote_archive.py` | tar/zip staging 压缩后的 `mv` 改为 `mv -f -- ...`。 |
| `tests/test_server_file_transfer.py` | 新增安全解压委托、解压校验错误、目标路径穿越拒绝、下载 staging move/rm 参数终止符测试;更新旧的 tar overwrite 行为测试。 |
| `tests/test_remote_archive_commands.py` | 新增 tar/zip staging move 参数终止符测试。 |
| `.skills/nexus-security-review/SKILL.md` | 固化 Nexus 安全巡检总规则。 |
| `.skills/nexus-ssh-safety/SKILL.md` | 固化 SSH 命令、路径、归档安全规则。 |
| `.skills/nexus-btpanel-review/SKILL.md` | 固化宝塔一键登录/凭据/会话修复审查规则。 |
## 4. 边界说明
- `script_service` 是管理员显式远程 shell 能力,本阶段没有把它作为命令注入漏洞处理;后续只审查鉴权、审计、超时、输出截断和脱敏。
- 文件传输仍允许管理员选择任意绝对目标路径,但不允许路径字符串中包含 `..`、反斜杠或非绝对路径。
- 归档解压会拒绝绝对路径、父目录穿越、Windows 反斜杠、符号链接、硬链接和设备/FIFO/socket 等特殊条目。
- 未在报告中记录任何密码、token、cookie、SSH key 或 BT API key。
## 5. 验证记录
```bash
.venv/bin/python -m pytest tests/test_server_file_transfer.py tests/test_remote_archive_commands.py -q
# 40 passed
.venv/bin/python -m pytest -q
# 748 passed, 1 skipped
git diff --check
# pass
```
## 6. 下一步建议
1. 继续审查 `server/api/servers.py` 的 agent 安装/升级/回滚命令拼接与超时边界。
2. 审查 `remote_shell.py` / `asyncssh_pool.py` 的 stdout/stderr 截断、敏感输出脱敏和 sudo fallback 一致性。
3. 审查 API 鉴权覆盖:确认所有管理接口均有管理员依赖,公开下载类接口只暴露必要的 token 校验。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,236 @@
# Nexus 手动发布命令清单(2026-07-08)
> 适用场景:Codex 远程 SSH 审批不可用时,由人工在本机和线上服务器执行。
> 发布包已通过本地 Gate 和发布包扫描。
## 本地发布包
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
## 方案 A:使用现有 deploy/deploy-production.sh(推荐)
前提:
- 本机能通过 SSH 访问线上 Host:`nexus`
- 或设置 `NEXUS_SSH=user@host`
- 线上已有 `/opt/nexus``/www/wwwroot/api.synaglobal.vip`
在源码快照目录执行:
```bash
cd /c/Users/uzuma/Documents/Codex/2026-07-06/yuu/work/nexus_code_context/snapshots/nexus-source-20260707-112042
bash deploy/deploy-production.sh
```
如果不用 SSH config
```bash
export NEXUS_SSH='root@你的线上服务器IP'
export NEXUS_DEPLOY_PATH='/opt/nexus'
bash deploy/deploy-production.sh
```
发布脚本会:
1. 本地同步源码到线上。
2. 排除 `.venv-*``node_modules`、缓存、误生成文件。
3. Docker 模式下执行 `nexus-1panel.sh upgrade --skip-git`
4. 构建并上传旧 `/app/`
5. 构建并上传新 `/app-v2/`
6. Docker 模式下同步 `web/app``web/app-v2` 到容器。
7. 检查 `/health``/app/``/app-v2/`
## 方案 B:发布 tar.gz 包到线上
### 1. 上传
Windows CMD / PowerShell
```cmd
scp C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz nexus:/tmp/nexus-release-20260708.tar.gz
```
### 2. 线上校验
```bash
cd /tmp
sha256sum nexus-release-20260708.tar.gz
```
必须等于:
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
### 3. 线上备份
假设部署目录为 `/opt/nexus`
```bash
sudo mkdir -p /opt/nexus-backups
sudo tar czf /opt/nexus-backups/nexus-before-20260708.tar.gz -C /opt nexus
```
### 3B. 使用服务器侧脚本自动应用发布包(推荐)
如果线上已存在当前项目的 `deploy/apply-release-bundle.sh`,或者你先把发布包解开到临时目录后使用新脚本,可以执行:
```bash
sudo bash /opt/nexus/deploy/apply-release-bundle.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
如果脚本尚未在旧线上目录中,可以先临时解出脚本:
```bash
cd /tmp
tar xzf nexus-release-20260708.tar.gz nexus-release/deploy/apply-release-bundle.sh
sudo bash /tmp/nexus-release/deploy/apply-release-bundle.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
这个脚本会自动:
1. 校验 SHA256。
2. 检查发布包内必须路径和禁止路径。
3. 备份当前 `/opt/nexus`
4. rsync 覆盖代码,但保留 `.env``docker/.env.prod``web/data`
5. Docker 模式下执行 `nexus-1panel.sh upgrade --skip-git`
6. 同步 `/app/``/app-v2/` 静态资源到容器。
7. 验证本机 `/health``/app/``/app-v2/`
### 4. 解包覆盖
```bash
sudo mkdir -p /opt/nexus
sudo tar xzf /tmp/nexus-release-20260708.tar.gz -C /tmp
sudo rsync -a --delete \
--exclude='.env' \
--exclude='docker/.env.prod' \
--exclude='web/data' \
/tmp/nexus-release/ /opt/nexus/
```
### 5. 重建/重启
Docker/1Panel 模式:
```bash
cd /opt/nexus
sudo env NEXUS_ROOT=/opt/nexus bash deploy/nexus-1panel.sh upgrade --skip-git
```
如果是 Supervisor 模式:
```bash
sudo supervisorctl restart nexus
```
### 6. Docker 容器内静态资源同步
Docker 模式建议执行:
```bash
cd /opt/nexus
sudo env NEXUS_ROOT=/opt/nexus bash deploy/sync_webapp_to_container.sh
```
## 方案 C:Python 发布辅助脚本(推荐替代手工 scp/ssh)
默认只上传并执行服务器侧 preflight,不会正式 apply
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
preflight 输出确认无误后,再加 `--apply` 正式发布:
```cmd
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^
--apply
```
详见:`Nexus-release-python-publisher-20260708.md`
## 发布后验证
本机执行:
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip
```
线上执行:
```bash
curl -s https://api.synaglobal.vip/health
curl -I https://api.synaglobal.vip/app/
curl -I https://api.synaglobal.vip/app-v2/
```
期望:
```text
/health => ok
/app/ => HTTP 200
/app-v2/ => HTTP 200
```
## 宝塔专项验证
发布后在 Nexus 后台验证:
1. 打开 `/app/` 旧后台,确认宝塔列表正常。
2. 打开 `/app-v2/` 新后台,确认页面可访问。
3. 对同一台宝塔连续点击一键登录,冲突时应该返回 HTTP 409。
4. 对新增宝塔执行一键登录,应触发 hardcoded_3600 检测/修复兜底。
5. 多台宝塔同时打开后,2 小时刷新不应因为 `task.py` hardcoded `3600` 陆续掉线。
## 回滚
如果异常:
```bash
cd /opt
sudo mv nexus nexus-bad-20260708
sudo tar xzf /opt/nexus-backups/nexus-before-20260708.tar.gz -C /opt
cd /opt/nexus
sudo env NEXUS_ROOT=/opt/nexus bash deploy/nexus-1panel.sh upgrade --skip-git
```
## Server preflight command
Before applying the release on the production host, run:
```bash
bash /tmp/nexus-release/deploy/preflight-release-host.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
This checks required commands, checksum, release tar paths, deploy path, runtime hints, disk space, and current local endpoints before applying changes.
@@ -0,0 +1,21 @@
# Nexus release artifact metadata
- File: `nexus-release-20260708.tar.gz`
- Size: `27.02 MB`
- Entries: `2409`
- SHA256: `46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b`
- Created UTC: `2026-07-08T09:59:20.278597+00:00`
## Verify after upload
Linux:
```bash
sha256sum nexus-release-20260708.tar.gz
```
PowerShell:
```powershell
Get-FileHash .\nexus-release-20260708.tar.gz -Algorithm SHA256
```
@@ -0,0 +1,777 @@
# Nexus release diff manifest (2026-07-08)
Baseline:
- Old snapshot: `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236`
- New snapshot: `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042`
Excluded names/patterns: `.git`, `node_modules`, `.venv-*`, `venv`, `__pycache__`, `.ruff_cache`, `.playwright-mcp`, `.cursor`, `.pytest_cache`, `tmp`, `*.pyc`, `*.pyo`, `*.bak-`, `2025.2`, `=2025.2`.
## Summary
- Added files: 381
- Modified files: 26
- Deleted files: 347
## Added files
- `.skills/nexus-btpanel-review/SKILL.md`
- `.skills/nexus-security-review/SKILL.md`
- `.skills/nexus-ssh-safety/SKILL.md`
- `deploy/apply-release-bundle.sh`
- `deploy/preflight-release-host.sh`
- `docs/security/Nexus-SSH鍛戒护鎵ц瀹夊叏宸℃闃舵3-鏂囦欢浼犺緭涓庝笓鐢⊿kill-20260707.md`
- `frontend-v2/index.html`
- `frontend-v2/package-lock.json`
- `frontend-v2/package.json`
- `frontend-v2/postcss.config.js`
- `frontend-v2/scripts/scan-app-v2-sensitive.mjs`
- `frontend-v2/src/App.tsx`
- `frontend-v2/src/components/ui/Badge.tsx`
- `frontend-v2/src/components/ui/Panel.tsx`
- `frontend-v2/src/features/dashboard/DashboardPage.tsx`
- `frontend-v2/src/features/dashboard/api/dashboardApi.ts`
- `frontend-v2/src/features/dashboard/components/AppShell.tsx`
- `frontend-v2/src/features/dashboard/components/AssetInventoryPage.tsx`
- `frontend-v2/src/features/dashboard/components/AuditLogPage.tsx`
- `frontend-v2/src/features/dashboard/components/AuditTimeline.tsx`
- `frontend-v2/src/features/dashboard/components/BtDomainSslPage.tsx`
- `frontend-v2/src/features/dashboard/components/BtPanelPage.tsx`
- `frontend-v2/src/features/dashboard/components/BtResourceOverviewPage.tsx`
- `frontend-v2/src/features/dashboard/components/CommandPalette.tsx`
- `frontend-v2/src/features/dashboard/components/ExecutionRecordsPage.tsx`
- `frontend-v2/src/features/dashboard/components/GlobalSearchPage.tsx`
- `frontend-v2/src/features/dashboard/components/HealthScore.tsx`
- `frontend-v2/src/features/dashboard/components/OperationLogsPage.tsx`
- `frontend-v2/src/features/dashboard/components/ReadOnlyFileBrowser.tsx`
- `frontend-v2/src/features/dashboard/components/ScriptLibraryPage.tsx`
- `frontend-v2/src/features/dashboard/components/SecurityInspectionPage.tsx`
- `frontend-v2/src/features/dashboard/components/ServerDrawer.tsx`
- `frontend-v2/src/features/dashboard/components/ServerTable.tsx`
- `frontend-v2/src/features/dashboard/components/SettingsOverviewPage.tsx`
- `frontend-v2/src/features/dashboard/components/StatCard.tsx`
- `frontend-v2/src/features/dashboard/components/TaskCenter.tsx`
- `frontend-v2/src/features/dashboard/components/TaskCenterPage.tsx`
- `frontend-v2/src/features/dashboard/components/TerminalFilesPage.tsx`
- `frontend-v2/src/features/dashboard/components/TrendPanel.tsx`
- `frontend-v2/src/features/dashboard/components/TtlMatrix.tsx`
- `frontend-v2/src/features/dashboard/components/WatchMonitoringPage.tsx`
- `frontend-v2/src/features/dashboard/data/dashboardData.ts`
- `frontend-v2/src/features/dashboard/hooks/useDashboardData.ts`
- `frontend-v2/src/lib/api.ts`
- `frontend-v2/src/lib/redaction.ts`
- `frontend-v2/src/lib/utils.ts`
- `frontend-v2/src/main.tsx`
- `frontend-v2/src/stores/appStore.ts`
- `frontend-v2/src/styles.css`
- `frontend-v2/src/vite-env.d.ts`
- `frontend-v2/tailwind.config.ts`
- `frontend-v2/tsconfig.json`
- `frontend-v2/tsconfig.node.json`
- `frontend-v2/vite.config.ts`
- `scripts/check_backend_test_env.py`
- `scripts/check_deploy_excludes.py`
- `scripts/check_text_integrity.py`
- `scripts/generate_release_bundle.py`
- `scripts/generate_release_diff.py`
- `scripts/prepare_release_artifact.py`
- `scripts/publish_release_bundle.py`
- `scripts/run_btpanel_tests.py`
- `scripts/run_btpanel_tests_docker.cmd`
- `scripts/run_btpanel_tests_docker.sh`
- `scripts/run_local_release_gate.py`
- `scripts/scan_release_bundle.py`
- `scripts/scan_release_manifest.py`
- `scripts/verify_online_release.py`
- `scripts/write_release_metadata.py`
- `tests/test_btpanel_bootstrap_lock.py`
- `tests/test_publish_release_bundle.py`
- `tests/test_remote_archive_commands.py`
- `web/app-v2/assets/DashboardPage--9cH96CJ.js`
- `web/app-v2/assets/index-8f2_PJZM.js`
- `web/app-v2/assets/index-saKc1OPG.css`
- `web/app-v2/assets/query-B5fiFlYp.js`
- `web/app-v2/assets/react-DGbf-iXy.js`
- `web/app-v2/assets/rolldown-runtime-Bh1tDfsg.js`
- `web/app-v2/assets/table-C-Cg70AL.js`
- `web/app-v2/assets/ui-sq2aW9r5.js`
- `web/app-v2/index.html`
- `web/app/assets/AlertsPage-BZa6JbW1.js`
- `web/app/assets/AlertsPage-DkbuUWgZ.js`
- `web/app/assets/AuditPage-CrzMBMPz.js`
- `web/app/assets/AuditPage-DE5F-bot.js`
- `web/app/assets/BtPanelCrontabPage-CsxFXzwR.js`
- `web/app/assets/BtPanelCrontabPage-DFAuKeNI.js`
- `web/app/assets/BtPanelDatabasesPage-Cua9aENZ.js`
- `web/app/assets/BtPanelDatabasesPage-Cxt_i4Lp.js`
- `web/app/assets/BtPanelDomainsPage-9oKLFhi4.js`
- `web/app/assets/BtPanelDomainsPage-CSupPzQH.js`
- `web/app/assets/BtPanelLoginPage-DiKTnvyN.js`
- `web/app/assets/BtPanelLoginPage-Dp3X10Er.js`
- `web/app/assets/BtPanelMonitorPage-2tHIkE0-.js`
- `web/app/assets/BtPanelMonitorPage-Pm3fw_df.js`
- `web/app/assets/BtPanelMonitorPage-Td-5siPa.css`
- `web/app/assets/BtPanelServicesPage-B652HAqe.js`
- `web/app/assets/BtPanelServicesPage-DR8K2l-F.js`
- `web/app/assets/BtPanelSettingsPage-BBi6ttQm.js`
- `web/app/assets/BtPanelSettingsPage-DCQKy6JG.js`
- `web/app/assets/BtPanelSiteCreatePage-DLG_BM-C.js`
- `web/app/assets/BtPanelSiteCreatePage-DTCrnEa5.js`
- `web/app/assets/BtPanelSitesPage--EP6G5x3.js`
- `web/app/assets/BtPanelSitesPage-DWm4LMaV.js`
- `web/app/assets/BtPanelSslPage-BuiQZ1OJ.js`
- `web/app/assets/BtPanelSslPage-JWzPztxi.js`
- `web/app/assets/CommandsPage-BNtXfIOs.js`
- `web/app/assets/CommandsPage-CuHLCAHz.js`
- `web/app/assets/CredentialsPage-BPUBEwg9.js`
- `web/app/assets/CredentialsPage-Dc92himy.js`
- `web/app/assets/DashboardPage-DOc8f-2E.js`
- `web/app/assets/DashboardPage-DmG38yGr.css`
- `web/app/assets/DashboardPage-yElaw2BV.js`
- `web/app/assets/FilesPage-Be2kmggp.js`
- `web/app/assets/FilesPage-By07WOZR.css`
- `web/app/assets/FilesPage-DHwnC_As.js`
- `web/app/assets/FleetTrendEChart-DbHCuhg7.css`
- `web/app/assets/FleetTrendEChart-Dz57W1gD.js`
- `web/app/assets/FleetTrendEChart-lgdTP3R-.js`
- `web/app/assets/LoginPage-3NZZLudg.js`
- `web/app/assets/LoginPage-BKIQ3yge.js`
- `web/app/assets/LoginPage-BTEVFKpe.css`
- `web/app/assets/PushPage-BcIAXgGU.js`
- `web/app/assets/PushPage-DnVPUcAX.css`
- `web/app/assets/PushPage-_afVGSg2.js`
- `web/app/assets/RetriesPage-BdJWTxtp.js`
- `web/app/assets/RetriesPage-C70vf571.js`
- `web/app/assets/SchedulesPage-B47mlwc8.js`
- `web/app/assets/SchedulesPage-B9PFTR71.css`
- `web/app/assets/SchedulesPage-DM0j6jps.js`
- `web/app/assets/ScriptRunsPage-CW2C9c7C.css`
- `web/app/assets/ScriptRunsPage-DIeeXpt5.js`
- `web/app/assets/ScriptRunsPage-DeZ4sRVP.js`
- `web/app/assets/ScriptsPage-BWoaZhKf.js`
- `web/app/assets/ScriptsPage-BrIbZLc5.css`
- `web/app/assets/ScriptsPage-D8An6uCU.js`
- `web/app/assets/ServerCategoryPicker-BJm8eTKs.js`
- `web/app/assets/ServerCategoryPicker-BlyQtEug.js`
- `web/app/assets/ServerCategoryPicker-CHn3BA4z.css`
- `web/app/assets/ServerQuickAddDialogs-CoSabmGi.js`
- `web/app/assets/ServerQuickAddDialogs-zcLgLArY.js`
- `web/app/assets/ServerTransferPage-BdwpJEc4.css`
- `web/app/assets/ServerTransferPage-CWGjaCXR.js`
- `web/app/assets/ServerTransferPage-UYjJ6gzQ.js`
- `web/app/assets/ServersPage-CEOh0Y1q.css`
- `web/app/assets/ServersPage-D7wE_7hM.css`
- `web/app/assets/ServersPage-nR5-sjDp.js`
- `web/app/assets/ServersPage-nhvNw2_Z.js`
- `web/app/assets/SettingsPage-Am37wCvf.js`
- `web/app/assets/SettingsPage-FAHRRqSI.js`
- `web/app/assets/StatCardsRow-CWt_wcFU.js`
- `web/app/assets/StatCardsRow-CdfBB9tI.js`
- `web/app/assets/StatCardsRow-snFLCRnD.css`
- `web/app/assets/TerminalPage-CNkogNbG.js`
- `web/app/assets/TerminalPage-DX9qLtN2.js`
- `web/app/assets/TerminalPage-DvGyoWff.css`
- `web/app/assets/VAutocomplete-B1NSIBuv.js`
- `web/app/assets/VAutocomplete-CZMI3cz9.js`
- `web/app/assets/VAutocomplete-D48FgPMA.css`
- `web/app/assets/VCard-BCYbpv-w.css`
- `web/app/assets/VCard-BwwttBcf.js`
- `web/app/assets/VCard-pmpQR9O7.js`
- `web/app/assets/VCheckbox-BO-mWtTk.js`
- `web/app/assets/VCheckbox-ChRKi1kj.css`
- `web/app/assets/VCheckbox-RnLQ4WBr.js`
- `web/app/assets/VDataTable-ClfE0-sD.js`
- `web/app/assets/VDataTable-D61zAwkI.css`
- `web/app/assets/VDataTable-pRRodit6.js`
- `web/app/assets/VDataTableServer-BIMNqMvk.js`
- `web/app/assets/VDataTableServer-BysXbZ8p.js`
- `web/app/assets/VDialog-BFQGuuXw.js`
- `web/app/assets/VDialog-DP-kc9Gx.css`
- `web/app/assets/VDialog-nKJcjfNL.js`
- `web/app/assets/VExpansionPanels-ByOIBgKv.js`
- `web/app/assets/VExpansionPanels-CUfe7Zz5.css`
- `web/app/assets/VExpansionPanels-DUlWPWNJ.js`
- `web/app/assets/VForm-CAvEgM-c.js`
- `web/app/assets/VForm-uEqKaHHF.js`
- `web/app/assets/VRow-DIr3-SqU.css`
- `web/app/assets/VRow-qjxp2VJ5.js`
- `web/app/assets/VRow-rmSa_q-C.js`
- `web/app/assets/VSelect-3h-EI23Y.js`
- `web/app/assets/VSelect-BmvHN1-i.css`
- `web/app/assets/VSelect-BvR-yyLS.js`
- `web/app/assets/VSkeletonLoader-BW_kZu-c.js`
- `web/app/assets/VSkeletonLoader-CkAIsbz1.css`
- `web/app/assets/VSkeletonLoader-gL4kF19X.js`
- `web/app/assets/VSwitch-CIruBPax.js`
- `web/app/assets/VSwitch-DPhHiAlh.css`
- `web/app/assets/VSwitch-VDVIZTO7.js`
- `web/app/assets/VTable-BTftprIQ.js`
- `web/app/assets/VTable-Dq_7MYoa.css`
- `web/app/assets/VTable-w1sxrmvd.js`
- `web/app/assets/VTabs-BX9iYxVa.js`
- `web/app/assets/VTabs-Bq6bCiCC.js`
- `web/app/assets/VTabs-DjRHpLZ2.css`
- `web/app/assets/VTextField-B-BnrJAQ.js`
- `web/app/assets/VTextField-Bl91kSVs.css`
- `web/app/assets/VTextField-GUQr1tUN.js`
- `web/app/assets/VTextarea-BMTWx02W.js`
- `web/app/assets/VTextarea-DDl63AeD.css`
- `web/app/assets/VTextarea-PzNEhaMZ.js`
- `web/app/assets/VTooltip-BbRT1OE8.js`
- `web/app/assets/VTooltip-aiKJ1MVA.css`
- `web/app/assets/VTooltip-tVMR683k.js`
- `web/app/assets/WatchMetricsPage-BjqkPE11.css`
- `web/app/assets/WatchMetricsPage-CL8abV00.js`
- `web/app/assets/WatchMetricsPage-DByjAcyM.css`
- `web/app/assets/WatchMetricsPage-DY4qY84I.js`
- `web/app/assets/_plugin-vue_export-helper-BDNMzG2s.js`
- `web/app/assets/abap-08VXUWAP.js`
- `web/app/assets/apex-BWPQTe0t.js`
- `web/app/assets/apiError-BUh3zCmq.js`
- `web/app/assets/apiError-CpgZRt4T.js`
- `web/app/assets/auditNormalize-xrm2MK_4.js`
- `web/app/assets/azcli-Bc_sGQ0U.js`
- `web/app/assets/bat-i0X4ZdIN.js`
- `web/app/assets/bicep-B5-_aFwp.js`
- `web/app/assets/btpanel-BsL25ffd.js`
- `web/app/assets/btpanel-C_41A_fE.js`
- `web/app/assets/cameligo-DMUM7wLl.js`
- `web/app/assets/chunk-DK3Fl9T5.js`
- `web/app/assets/clojure-Cm7r79vr.js`
- `web/app/assets/codicon-ngg6Pgfi.ttf`
- `web/app/assets/coffee-Ba7i2nA0.js`
- `web/app/assets/cpp-C7h46wYY.js`
- `web/app/assets/csharp-BKxtCVv1.js`
- `web/app/assets/csp-bTuwJoIa.js`
- `web/app/assets/css-DIMkf-bt.js`
- `web/app/assets/css.worker-CvXBzhp8.js`
- `web/app/assets/cssMode-CnBxfcpP.js`
- `web/app/assets/cssMode-DoYviH0F.js`
- `web/app/assets/cypher-CVaqCwHa.js`
- `web/app/assets/dart-onAF5SnQ.js`
- `web/app/assets/dataTable-esDY4cOk.js`
- `web/app/assets/dist-DCycUQQT.js`
- `web/app/assets/dist-DMqKEX4A.js`
- `web/app/assets/dockerfile-DZFCIeNp.js`
- `web/app/assets/ecl-D05T4iGw.js`
- `web/app/assets/editor.worker-Cn2oRESe.js`
- `web/app/assets/elixir-6RTg0lbw.js`
- `web/app/assets/execDetailGrouping-B4lJ15VJ.js`
- `web/app/assets/execDetailGrouping-CuMwXaZZ.js`
- `web/app/assets/flow9-C5_-GSwl.js`
- `web/app/assets/freemarker2-BUYL0LB-.js`
- `web/app/assets/freemarker2-D0e9LerR.js`
- `web/app/assets/fsharp-C8Ef5oNN.js`
- `web/app/assets/go-C-y9NEjX.js`
- `web/app/assets/graphql-fmXr3nnJ.js`
- `web/app/assets/handlebars-C8vPzJIb.js`
- `web/app/assets/handlebars-ncsxj4cu.js`
- `web/app/assets/hcl-CpzslTdj.js`
- `web/app/assets/html-8-zTuU_5.js`
- `web/app/assets/html-DUT2IBNz.js`
- `web/app/assets/html.worker-BO6WuOEO.js`
- `web/app/assets/htmlMode-Bm_X9rOD.js`
- `web/app/assets/htmlMode-C8LJuzCB.js`
- `web/app/assets/index-BNJe_ogF.css`
- `web/app/assets/index-CDcgx9V8.js`
- `web/app/assets/index-a0fi5f-0.js`
- `web/app/assets/ini-sBoK_t0W.js`
- `web/app/assets/initMonaco-Cjv05FdX.js`
- `web/app/assets/initMonaco-DTDMLv7Z.js`
- `web/app/assets/initMonaco-DX2BVZF3.css`
- `web/app/assets/install-BY4V-NK4.js`
- `web/app/assets/install-bdNoH-9Q.js`
- `web/app/assets/java-BEtHBSE6.js`
- `web/app/assets/javascript-B4NqljlL.js`
- `web/app/assets/javascript-wfMVVC84.js`
- `web/app/assets/json.worker-BkJRGcCJ.js`
- `web/app/assets/jsonMode-B06-mTjs.js`
- `web/app/assets/jsonMode-B7uld_NH.js`
- `web/app/assets/julia-Bri6UV-V.js`
- `web/app/assets/kotlin-BOotOW0E.js`
- `web/app/assets/labels-C-A2d08W.js`
- `web/app/assets/less-B9JPFI3C.js`
- `web/app/assets/lexon-CfSJPG6W.js`
- `web/app/assets/liquid-B1dfVIHc.js`
- `web/app/assets/liquid-BxkVsnPx.js`
- `web/app/assets/lspLanguageFeatures-3WI5pdzZ.js`
- `web/app/assets/lspLanguageFeatures-CaMGOy4a.js`
- `web/app/assets/lua-CsQS60Ue.js`
- `web/app/assets/m3-D-oSqn_W.js`
- `web/app/assets/markdown-Cimd5fb3.js`
- `web/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf`
- `web/app/assets/materialdesignicons-webfont-CSr8KVlo.eot`
- `web/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2`
- `web/app/assets/materialdesignicons-webfont-PXm3-2wK.woff`
- `web/app/assets/mdx-B6lamGpH.js`
- `web/app/assets/mdx-lREt6kXe.js`
- `web/app/assets/mips-CIPQ_RoX.js`
- `web/app/assets/msdax-DauUninz.js`
- `web/app/assets/mysql-SOo6toE5.js`
- `web/app/assets/objective-c-FvmIjYaQ.js`
- `web/app/assets/paginatedFetch-CijkqaDG.js`
- `web/app/assets/paginatedFetch-VYTwXwno.js`
- `web/app/assets/pascal-DrH0SRf2.js`
- `web/app/assets/pascaligo-D-ptJ9y-.js`
- `web/app/assets/perl-oz_6vUea.js`
- `web/app/assets/pgsql-DTj74zXo.js`
- `web/app/assets/php-nr791fC2.js`
- `web/app/assets/pla-CopQ2nXW.js`
- `web/app/assets/postiats-43DmfD33.js`
- `web/app/assets/powerquery-D3hlyOfw.js`
- `web/app/assets/powershell-DmHpPYUd.js`
- `web/app/assets/protobuf-C531GsRP.js`
- `web/app/assets/pug-Z5eAx3Zn.js`
- `web/app/assets/python-Dt0lyPW-.js`
- `web/app/assets/python-DwgZPXhL.js`
- `web/app/assets/qsharp-DkqhCAOL.js`
- `web/app/assets/r-BwWrilGY.js`
- `web/app/assets/razor-B1t_g3Ke.js`
- `web/app/assets/razor-Dv9uiya3.js`
- `web/app/assets/redis-ClamHrr6.js`
- `web/app/assets/redshift-DT7zqm-g.js`
- `web/app/assets/remotePath-D3rG2Z3e.js`
- `web/app/assets/restructuredtext-BYgofb2h.js`
- `web/app/assets/roboto-latin-300-normal-BARJ-h6h.woff`
- `web/app/assets/roboto-latin-300-normal-CCzlftfr.woff2`
- `web/app/assets/roboto-latin-400-normal-BqEyEoaF.woff2`
- `web/app/assets/roboto-latin-400-normal-DyYNIH4P.woff`
- `web/app/assets/roboto-latin-500-normal-7RbcRiD8.woff2`
- `web/app/assets/roboto-latin-500-normal-DQZyH_nt.woff`
- `web/app/assets/roboto-latin-700-normal-BZpUvMxY.woff2`
- `web/app/assets/roboto-latin-700-normal-DLgJJpmK.woff`
- `web/app/assets/ruby-DezsRK8O.js`
- `web/app/assets/rust-DdL9SqIa.js`
- `web/app/assets/sb-CcwsVR0C.js`
- `web/app/assets/scala-DHpiXF5c.js`
- `web/app/assets/scheme-BeGwcela.js`
- `web/app/assets/scss-gp-XZpBa.js`
- `web/app/assets/serverSiteUrl-DDZRgSNA.js`
- `web/app/assets/shell-CC2rA5mh.js`
- `web/app/assets/solidity-BEEn4gHE.js`
- `web/app/assets/sophia-CRfGWb83.js`
- `web/app/assets/sparql-D_Lu-MrJ.js`
- `web/app/assets/sql-NEE52Syq.js`
- `web/app/assets/st-DbInun42.js`
- `web/app/assets/status-BrGQYcd-.js`
- `web/app/assets/status-XDgjPQrL.js`
- `web/app/assets/swift-Bxkupp3x.js`
- `web/app/assets/systemverilog-Bz4Y3fRF.js`
- `web/app/assets/tcl-DISqw1ZD.js`
- `web/app/assets/ts.worker-B6r0Skfj.js`
- `web/app/assets/tsMode-9bXRGx5g.js`
- `web/app/assets/tsMode-ChdzhlZm.js`
- `web/app/assets/twig-De2hgUGE.js`
- `web/app/assets/typescript-D3T7er6k.js`
- `web/app/assets/typescript-DRN2FRNg.js`
- `web/app/assets/typespec-B8J7ngcE.js`
- `web/app/assets/useBtPanelLogin-DCNB-U_J.js`
- `web/app/assets/useBtPanelLogin-DDcleXPj.js`
- `web/app/assets/useBtPanelPageLoad-CD81wbTP.js`
- `web/app/assets/useBtPanelPageLoad-CV_unS2-.js`
- `web/app/assets/usePageActivateRefresh-BntaOABK.js`
- `web/app/assets/usePageActivateRefresh-CqHSpagO.js`
- `web/app/assets/usePageAutoRefresh-DW8DyVQQ.js`
- `web/app/assets/usePageAutoRefresh-DvG6Fd0_.js`
- `web/app/assets/usePushStaging-BCIq1TPK.js`
- `web/app/assets/usePushStaging-CfCU7s8k.css`
- `web/app/assets/usePushStaging-DcgeHeSX.js`
- `web/app/assets/useServerCategories-BcvoZF0H.js`
- `web/app/assets/useServerCategories-V7luFEnW.js`
- `web/app/assets/useServerList-6fsbQxWS.js`
- `web/app/assets/useServerList-w3j3GRMG.js`
- `web/app/assets/useServerSearchHistory-BnDuA-7p.js`
- `web/app/assets/useServerSearchHistory-C3ha-zP2.css`
- `web/app/assets/useServerSearchHistory-CTnIHvTd.js`
- `web/app/assets/useSnackbar-u_XK4ZNm.js`
- `web/app/assets/useTerminalQuickCommands-CGHoPHjs.js`
- `web/app/assets/useTerminalQuickCommands-DKqOac6p.js`
- `web/app/assets/useTerminalQuickCommands-DyOcH0CY.css`
- `web/app/assets/useWatchPins-DTUw7j9v.js`
- `web/app/assets/useWatchPins-DxD91kwD.js`
- `web/app/assets/validation-BMzYTJL4.js`
- `web/app/assets/vb-DV3o63ZY.js`
- `web/app/assets/wgsl-DpFanUEy.js`
- `web/app/assets/xml-BFdUA9kH.js`
- `web/app/assets/xml-Btmx4yuJ.js`
- `web/app/assets/yaml-DKn-1xmA.js`
- `web/app/assets/yaml-tUm_O8N4.js`
## Modified files
- `.dockerignore`
- `Dockerfile`
- `Dockerfile.prod`
- `deploy/deploy-production.sh`
- `deploy/rsync-local-to-server.sh`
- `deploy/sync_webapp_to_container.sh`
- `docker-compose.yml`
- `docs/research/jumpserver-easynode-architecture-analysis.md`
- `requirements-dev.txt`
- `server/api/assets.py`
- `server/api/scripts.py`
- `server/api/sync_v2.py`
- `server/application/services/btpanel_service.py`
- `server/application/services/server_file_transfer_service.py`
- `server/infrastructure/btpanel/bootstrap_lock.py`
- `server/infrastructure/btpanel/login_url_lock.py`
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `server/infrastructure/ssh/remote_archive.py`
- `server/main.py`
- `server/utils/files_chmod_policy.py`
- `tests/test_btpanel_get_config.py`
- `tests/test_btpanel_login_url.py`
- `tests/test_btpanel_login_url_route.py`
- `tests/test_btpanel_ssh_bootstrap.py`
- `tests/test_file_permissions.py`
- `tests/test_server_file_transfer.py`
## Deleted files
- `docker/.env.example`
- `docker/.env.prod.example`
- `docker/profiles/1c4g.env`
- `docker/profiles/2c8g.env`
- `docker/profiles/4c16g.env`
- `docs/Nexus-功能说明书.html`
- `frontend/.env.development`
- `scripts/npm-proxy.env`
- `web/app/dist/assets/AlertsPage-ji5eXKTc.js`
- `web/app/dist/assets/AuditPage-C3fmqt6f.js`
- `web/app/dist/assets/CommandsPage-C6QrgHcR.js`
- `web/app/dist/assets/CredentialsPage-oLbFLgd2.js`
- `web/app/dist/assets/DashboardPage-CKfPpNOT.js`
- `web/app/dist/assets/FilesPage-BvG082ru.css`
- `web/app/dist/assets/FilesPage-DNKIBjNs.js`
- `web/app/dist/assets/LoginPage-D8ZqP1P_.js`
- `web/app/dist/assets/MonacoEditor-Sfe2OjtA.js`
- `web/app/dist/assets/PushPage-BDdmIVXZ.js`
- `web/app/dist/assets/PushPage-ewjA8E7l.css`
- `web/app/dist/assets/RetriesPage-BJbJspGS.js`
- `web/app/dist/assets/SchedulesPage-D6oD8ext.js`
- `web/app/dist/assets/ScriptsPage-CbZT-hk0.js`
- `web/app/dist/assets/ServersPage-B4elP3Z1.js`
- `web/app/dist/assets/ServersPage-DjRHpLZ2.css`
- `web/app/dist/assets/SettingsPage-BuPIwZWo.js`
- `web/app/dist/assets/TerminalPage-BGKczTgd.js`
- `web/app/dist/assets/TerminalPage-Ol9WjDlo.css`
- `web/app/dist/assets/VAlert-B33BooF0.css`
- `web/app/dist/assets/VAlert-DGGVwUFF.js`
- `web/app/dist/assets/VDataTable-0Xurhrxv.js`
- `web/app/dist/assets/VDataTable-DlpDfCoY.css`
- `web/app/dist/assets/VDataTableServer-FvIQviSN.js`
- `web/app/dist/assets/VDialog-BLibPcoM.js`
- `web/app/dist/assets/VDialog-DP-kc9Gx.css`
- `web/app/dist/assets/VFileInput-C6sFcwsM.js`
- `web/app/dist/assets/VFileInput-HYy7i9zh.css`
- `web/app/dist/assets/VRow-DIr3-SqU.css`
- `web/app/dist/assets/VRow-r8nkWmtD.js`
- `web/app/dist/assets/VSelect-ALpjlDrA.css`
- `web/app/dist/assets/VSelect-Bz5bHvpa.js`
- `web/app/dist/assets/VSelectionControl-BTVr3HQm.css`
- `web/app/dist/assets/VSelectionControl-C1Y9bNOF.js`
- `web/app/dist/assets/VSpacer-DWZdSXUK.js`
- `web/app/dist/assets/VSpacer-DfbUir7X.css`
- `web/app/dist/assets/VSwitch-DPhHiAlh.css`
- `web/app/dist/assets/VSwitch-DY1GlILd.js`
- `web/app/dist/assets/VTextarea-3TieuoAv.js`
- `web/app/dist/assets/VTextarea-DDl63AeD.css`
- `web/app/dist/assets/abap-08VXUWAP.js`
- `web/app/dist/assets/apex-BWPQTe0t.js`
- `web/app/dist/assets/azcli-Bc_sGQ0U.js`
- `web/app/dist/assets/bat-i0X4ZdIN.js`
- `web/app/dist/assets/bicep-B5-_aFwp.js`
- `web/app/dist/assets/cameligo-DMUM7wLl.js`
- `web/app/dist/assets/clojure-Cm7r79vr.js`
- `web/app/dist/assets/codicon-ngg6Pgfi.ttf`
- `web/app/dist/assets/coffee-Ba7i2nA0.js`
- `web/app/dist/assets/cpp-C7h46wYY.js`
- `web/app/dist/assets/csharp-BKxtCVv1.js`
- `web/app/dist/assets/csp-bTuwJoIa.js`
- `web/app/dist/assets/css-DIMkf-bt.js`
- `web/app/dist/assets/css.worker-CvXBzhp8.js`
- `web/app/dist/assets/cssMode-DMvENJ_w.js`
- `web/app/dist/assets/cypher-CVaqCwHa.js`
- `web/app/dist/assets/dart-onAF5SnQ.js`
- `web/app/dist/assets/dockerfile-DZFCIeNp.js`
- `web/app/dist/assets/ecl-D05T4iGw.js`
- `web/app/dist/assets/editor-DX2BVZF3.css`
- `web/app/dist/assets/editor.api2-MWf-eAi-.js`
- `web/app/dist/assets/elixir-6RTg0lbw.js`
- `web/app/dist/assets/flow9-C5_-GSwl.js`
- `web/app/dist/assets/freemarker2-DQGAilFk.js`
- `web/app/dist/assets/fsharp-C8Ef5oNN.js`
- `web/app/dist/assets/go-C-y9NEjX.js`
- `web/app/dist/assets/graphql-fmXr3nnJ.js`
- `web/app/dist/assets/handlebars-DCagTjhi.js`
- `web/app/dist/assets/hcl-CpzslTdj.js`
- `web/app/dist/assets/html-nrfydv4h.js`
- `web/app/dist/assets/html.worker-BO6WuOEO.js`
- `web/app/dist/assets/htmlMode-Bp4WNwyQ.js`
- `web/app/dist/assets/index-DOFNruam.js`
- `web/app/dist/assets/index-YClpuO23.css`
- `web/app/dist/assets/ini-sBoK_t0W.js`
- `web/app/dist/assets/java-BEtHBSE6.js`
- `web/app/dist/assets/javascript-B0RcszDa.js`
- `web/app/dist/assets/json.worker-BkJRGcCJ.js`
- `web/app/dist/assets/jsonMode-D0vdA3eU.js`
- `web/app/dist/assets/julia-Bri6UV-V.js`
- `web/app/dist/assets/kotlin-BOotOW0E.js`
- `web/app/dist/assets/less-B9JPFI3C.js`
- `web/app/dist/assets/lexon-CfSJPG6W.js`
- `web/app/dist/assets/liquid-DAtIwle-.js`
- `web/app/dist/assets/lspLanguageFeatures-DRDm87pn.js`
- `web/app/dist/assets/lua-CsQS60Ue.js`
- `web/app/dist/assets/m3-D-oSqn_W.js`
- `web/app/dist/assets/markdown-Cimd5fb3.js`
- `web/app/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf`
- `web/app/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot`
- `web/app/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2`
- `web/app/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff`
- `web/app/dist/assets/mdx-BvtNR28y.js`
- `web/app/dist/assets/mips-CIPQ_RoX.js`
- `web/app/dist/assets/monaco.contribution-fyH2rvpO.js`
- `web/app/dist/assets/msdax-DauUninz.js`
- `web/app/dist/assets/mysql-SOo6toE5.js`
- `web/app/dist/assets/objective-c-FvmIjYaQ.js`
- `web/app/dist/assets/pascal-DrH0SRf2.js`
- `web/app/dist/assets/pascaligo-D-ptJ9y-.js`
- `web/app/dist/assets/perl-oz_6vUea.js`
- `web/app/dist/assets/pgsql-DTj74zXo.js`
- `web/app/dist/assets/php-nr791fC2.js`
- `web/app/dist/assets/pla-CopQ2nXW.js`
- `web/app/dist/assets/postiats-43DmfD33.js`
- `web/app/dist/assets/powerquery-D3hlyOfw.js`
- `web/app/dist/assets/powershell-DmHpPYUd.js`
- `web/app/dist/assets/protobuf-C531GsRP.js`
- `web/app/dist/assets/pug-Z5eAx3Zn.js`
- `web/app/dist/assets/python-Y2lQRCrb.js`
- `web/app/dist/assets/qsharp-DkqhCAOL.js`
- `web/app/dist/assets/r-BwWrilGY.js`
- `web/app/dist/assets/razor-DHI-mrmN.js`
- `web/app/dist/assets/redis-ClamHrr6.js`
- `web/app/dist/assets/redshift-DT7zqm-g.js`
- `web/app/dist/assets/restructuredtext-BYgofb2h.js`
- `web/app/dist/assets/roboto-cyrillic-100-italic-CNP2SmvR.woff2`
- `web/app/dist/assets/roboto-cyrillic-100-italic-GfU4zM_J.woff`
- `web/app/dist/assets/roboto-cyrillic-100-normal-D_oR061d.woff2`
- `web/app/dist/assets/roboto-cyrillic-100-normal-w5umKD67.woff`
- `web/app/dist/assets/roboto-cyrillic-300-italic-Bbg0zipm.woff2`
- `web/app/dist/assets/roboto-cyrillic-300-italic-BfviqnaP.woff`
- `web/app/dist/assets/roboto-cyrillic-300-normal-DEFNdjk5.woff`
- `web/app/dist/assets/roboto-cyrillic-300-normal-DzUz0kzv.woff2`
- `web/app/dist/assets/roboto-cyrillic-400-italic-BSQCxleC.woff`
- `web/app/dist/assets/roboto-cyrillic-400-italic-CqC_ywG3.woff2`
- `web/app/dist/assets/roboto-cyrillic-400-normal-Bjg-1-sg.woff`
- `web/app/dist/assets/roboto-cyrillic-400-normal-CBPI_iaY.woff2`
- `web/app/dist/assets/roboto-cyrillic-500-italic-CFOZTHyM.woff2`
- `web/app/dist/assets/roboto-cyrillic-500-italic-UnqpRRY_.woff`
- `web/app/dist/assets/roboto-cyrillic-500-normal-CBKMylY4.woff`
- `web/app/dist/assets/roboto-cyrillic-500-normal-CLao9AfR.woff2`
- `web/app/dist/assets/roboto-cyrillic-700-italic-DhNJyfCT.woff2`
- `web/app/dist/assets/roboto-cyrillic-700-italic-K45OuYTL.woff`
- `web/app/dist/assets/roboto-cyrillic-700-normal-C2o7G-SM.woff2`
- `web/app/dist/assets/roboto-cyrillic-700-normal-DhZFXDSN.woff`
- `web/app/dist/assets/roboto-cyrillic-900-italic-CW7hKEUp.woff2`
- `web/app/dist/assets/roboto-cyrillic-900-italic-Dw8LBt_T.woff`
- `web/app/dist/assets/roboto-cyrillic-900-normal-B-XH5ueX.woff`
- `web/app/dist/assets/roboto-cyrillic-900-normal-BVOxCBIE.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-100-italic-CHVV4rI0.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-100-italic-Drj7gMwC.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-100-normal-mbO7vZh1.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-100-normal-uxSc4Dbo.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-300-italic-D2iEI_X7.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-300-italic-OnrzXwVD.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-300-normal-D7ank4TF.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-300-normal-DIxttMbC.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-400-italic-4ESj9BbU.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-400-italic-MZ-G6OiW.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-400-normal-CaK1767H.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-400-normal-qHufge6k.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-500-italic-C-yqETei.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-500-italic-ExZ7-vA1.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-500-normal-BWC_xYeb.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-500-normal-DqF2hftb.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-700-italic-BPGsVrTY.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-700-italic-DEF-7i1Y.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-700-normal-CI7FH63F.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-700-normal-DmFxo5wj.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-900-italic-CSUk7NP9.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-900-italic-acKlpJv6.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-900-normal-Dy18Zgm9.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-900-normal-IZ2B0aiV.woff2`
- `web/app/dist/assets/roboto-greek-100-italic-BByoHCxw.woff2`
- `web/app/dist/assets/roboto-greek-100-italic-Cy3jdfDA.woff`
- `web/app/dist/assets/roboto-greek-100-normal-DgpMWfbq.woff2`
- `web/app/dist/assets/roboto-greek-100-normal-ZuTz319d.woff`
- `web/app/dist/assets/roboto-greek-300-italic-BlmQgaZh.woff`
- `web/app/dist/assets/roboto-greek-300-italic-CznjWsFz.woff2`
- `web/app/dist/assets/roboto-greek-300-normal-C_Dgaih9.woff`
- `web/app/dist/assets/roboto-greek-300-normal-DJEM9B4Z.woff2`
- `web/app/dist/assets/roboto-greek-400-italic-BUWCAFe1.woff`
- `web/app/dist/assets/roboto-greek-400-italic-DnTUMkdF.woff2`
- `web/app/dist/assets/roboto-greek-400-normal-Bb5mj_fZ.woff`
- `web/app/dist/assets/roboto-greek-400-normal-ai2Z1K3C.woff2`
- `web/app/dist/assets/roboto-greek-500-italic-Dqz20l7v.woff2`
- `web/app/dist/assets/roboto-greek-500-italic-xdwGU0k_.woff`
- `web/app/dist/assets/roboto-greek-500-normal-C9AnhcmC.woff2`
- `web/app/dist/assets/roboto-greek-500-normal-oCqhoyfc.woff`
- `web/app/dist/assets/roboto-greek-700-italic-DawR0hnC.woff2`
- `web/app/dist/assets/roboto-greek-700-italic-P6YHZuSy.woff`
- `web/app/dist/assets/roboto-greek-700-normal-0aHWxGLu.woff2`
- `web/app/dist/assets/roboto-greek-700-normal-DjRqqLBV.woff`
- `web/app/dist/assets/roboto-greek-900-italic-B7xCHAj9.woff`
- `web/app/dist/assets/roboto-greek-900-italic-DvS3zuic.woff2`
- `web/app/dist/assets/roboto-greek-900-normal-B5AAzeOC.woff2`
- `web/app/dist/assets/roboto-greek-900-normal-DWdVoZCP.woff`
- `web/app/dist/assets/roboto-latin-100-italic-CMH2tK0H.woff2`
- `web/app/dist/assets/roboto-latin-100-italic-DZWoXrp_.woff`
- `web/app/dist/assets/roboto-latin-100-normal-CGMktwvD.woff`
- `web/app/dist/assets/roboto-latin-100-normal-vTzS_GaG.woff2`
- `web/app/dist/assets/roboto-latin-300-italic-B5i8yzYq.woff`
- `web/app/dist/assets/roboto-latin-300-italic-zCpkrxSM.woff2`
- `web/app/dist/assets/roboto-latin-300-normal-BARJ-h6h.woff`
- `web/app/dist/assets/roboto-latin-300-normal-CCzlftfr.woff2`
- `web/app/dist/assets/roboto-latin-400-italic-C2a9rKC1.woff`
- `web/app/dist/assets/roboto-latin-400-italic-CSuqwwKr.woff2`
- `web/app/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2`
- `web/app/dist/assets/roboto-latin-400-normal-DyYNIH4P.woff`
- `web/app/dist/assets/roboto-latin-500-italic-Bzx6Emny.woff`
- `web/app/dist/assets/roboto-latin-500-italic-DOe3GFcv.woff2`
- `web/app/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2`
- `web/app/dist/assets/roboto-latin-500-normal-DQZyH_nt.woff`
- `web/app/dist/assets/roboto-latin-700-italic-CC4lh_E9.woff`
- `web/app/dist/assets/roboto-latin-700-italic-Cb9agdGy.woff2`
- `web/app/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2`
- `web/app/dist/assets/roboto-latin-700-normal-DLgJJpmK.woff`
- `web/app/dist/assets/roboto-latin-900-italic-BENCS7Df.woff2`
- `web/app/dist/assets/roboto-latin-900-italic-DNjw4RGV.woff`
- `web/app/dist/assets/roboto-latin-900-normal-F72S18P8.woff`
- `web/app/dist/assets/roboto-latin-900-normal-lk0O8k6m.woff2`
- `web/app/dist/assets/roboto-latin-ext-100-italic-BzoOrAFP.woff`
- `web/app/dist/assets/roboto-latin-ext-100-italic-CprbSwJf.woff2`
- `web/app/dist/assets/roboto-latin-ext-100-normal-Bue4UH9m.woff2`
- `web/app/dist/assets/roboto-latin-ext-100-normal-CRE1JcN2.woff`
- `web/app/dist/assets/roboto-latin-ext-300-italic-BaJRnD9g.woff`
- `web/app/dist/assets/roboto-latin-ext-300-italic-Bavh33sq.woff2`
- `web/app/dist/assets/roboto-latin-ext-300-normal-B90pq-BC.woff2`
- `web/app/dist/assets/roboto-latin-ext-300-normal-CTCCHkZF.woff`
- `web/app/dist/assets/roboto-latin-ext-400-italic-BilqUbDw.woff2`
- `web/app/dist/assets/roboto-latin-ext-400-italic-g8iNtijM.woff`
- `web/app/dist/assets/roboto-latin-ext-400-normal-C3tdtHj3.woff2`
- `web/app/dist/assets/roboto-latin-ext-400-normal-scX0fKtV.woff`
- `web/app/dist/assets/roboto-latin-ext-500-italic-B5teBpxj.woff2`
- `web/app/dist/assets/roboto-latin-ext-500-italic-BlZHPPdA.woff`
- `web/app/dist/assets/roboto-latin-ext-500-normal-Cyc0AKLz.woff`
- `web/app/dist/assets/roboto-latin-ext-500-normal-pMCM9Ixg.woff2`
- `web/app/dist/assets/roboto-latin-ext-700-italic-C8tIie4u.woff`
- `web/app/dist/assets/roboto-latin-ext-700-italic-CBdFfgzf.woff2`
- `web/app/dist/assets/roboto-latin-ext-700-normal-BUhwtWwy.woff`
- `web/app/dist/assets/roboto-latin-ext-700-normal-DSBUz0N1.woff2`
- `web/app/dist/assets/roboto-latin-ext-900-italic-CFogYjYt.woff`
- `web/app/dist/assets/roboto-latin-ext-900-italic-CgK4zYaI.woff2`
- `web/app/dist/assets/roboto-latin-ext-900-normal-Bg1HnWxG.woff2`
- `web/app/dist/assets/roboto-latin-ext-900-normal-CUx1IrMY.woff`
- `web/app/dist/assets/roboto-math-100-italic-B4vNMY8j.woff`
- `web/app/dist/assets/roboto-math-100-italic-C4gy5Bp3.woff2`
- `web/app/dist/assets/roboto-math-100-normal-3u4f34A3.woff2`
- `web/app/dist/assets/roboto-math-100-normal-DzbsQ8e_.woff`
- `web/app/dist/assets/roboto-math-300-italic-CIRUIJB7.woff`
- `web/app/dist/assets/roboto-math-300-italic-XxAa5nOs.woff2`
- `web/app/dist/assets/roboto-math-300-normal-5dF_7mZP.woff2`
- `web/app/dist/assets/roboto-math-300-normal-Ds0YpBw2.woff`
- `web/app/dist/assets/roboto-math-400-italic-BpybG2ZH.woff2`
- `web/app/dist/assets/roboto-math-400-italic-CXOsqNh0.woff`
- `web/app/dist/assets/roboto-math-400-normal-BEFej5gc.woff2`
- `web/app/dist/assets/roboto-math-400-normal-C9RxBKAh.woff`
- `web/app/dist/assets/roboto-math-500-italic-BaDHGWdF.woff2`
- `web/app/dist/assets/roboto-math-500-italic-Dr-Gj3Mh.woff`
- `web/app/dist/assets/roboto-math-500-normal-C-7mKPO3.woff`
- `web/app/dist/assets/roboto-math-500-normal-C4NU9gLX.woff2`
- `web/app/dist/assets/roboto-math-700-italic-B5uBDPR4.woff`
- `web/app/dist/assets/roboto-math-700-italic-BTlAqkAP.woff2`
- `web/app/dist/assets/roboto-math-700-normal-B8YqGHVc.woff2`
- `web/app/dist/assets/roboto-math-700-normal-DVoD5t2k.woff`
- `web/app/dist/assets/roboto-math-900-italic-B-PPazk1.woff`
- `web/app/dist/assets/roboto-math-900-italic-D-7BUet6.woff2`
- `web/app/dist/assets/roboto-math-900-normal-DQ66ivDi.woff2`
- `web/app/dist/assets/roboto-math-900-normal-Dmeiz_CW.woff`
- `web/app/dist/assets/roboto-symbols-100-italic-DSe_C1Iy.woff`
- `web/app/dist/assets/roboto-symbols-100-italic-DkwGdnY5.woff2`
- `web/app/dist/assets/roboto-symbols-100-normal-CjKjWFkd.woff2`
- `web/app/dist/assets/roboto-symbols-100-normal-CqmTeVyX.woff`
- `web/app/dist/assets/roboto-symbols-300-italic-BfpS6Q35.woff2`
- `web/app/dist/assets/roboto-symbols-300-italic-Dm46KJRd.woff`
- `web/app/dist/assets/roboto-symbols-300-normal-BCnjhQd_.woff`
- `web/app/dist/assets/roboto-symbols-300-normal-DDU7avhj.woff2`
- `web/app/dist/assets/roboto-symbols-400-italic-CQIwDYNA.woff`
- `web/app/dist/assets/roboto-symbols-400-italic-DJOHuWBY.woff2`
- `web/app/dist/assets/roboto-symbols-400-normal-CB1Ce4Gk.woff2`
- `web/app/dist/assets/roboto-symbols-400-normal-DLYbZahX.woff`
- `web/app/dist/assets/roboto-symbols-500-italic-BiGHMB5Q.woff2`
- `web/app/dist/assets/roboto-symbols-500-italic-Dz3aAlfh.woff`
- `web/app/dist/assets/roboto-symbols-500-normal-B_CZKVJS.woff2`
- `web/app/dist/assets/roboto-symbols-500-normal-F7c8nfcH.woff`
- `web/app/dist/assets/roboto-symbols-700-italic-Bj79QYOr.woff2`
- `web/app/dist/assets/roboto-symbols-700-italic-Dm-8FOP3.woff`
- `web/app/dist/assets/roboto-symbols-700-normal-BiFDindJ.woff2`
- `web/app/dist/assets/roboto-symbols-700-normal-BoS6HWkc.woff`
- `web/app/dist/assets/roboto-symbols-900-italic-BZDs0BY5.woff`
- `web/app/dist/assets/roboto-symbols-900-italic-QOKtSfbw.woff2`
- `web/app/dist/assets/roboto-symbols-900-normal-1vlkxR2C.woff2`
- `web/app/dist/assets/roboto-symbols-900-normal-9XmQV1ku.woff`
- `web/app/dist/assets/roboto-vietnamese-100-italic-BnSYtt9K.woff2`
- `web/app/dist/assets/roboto-vietnamese-100-italic-Hu6kAu-e.woff`
- `web/app/dist/assets/roboto-vietnamese-100-normal-Cc5a3-TP.woff`
- `web/app/dist/assets/roboto-vietnamese-100-normal-KgOkQYnu.woff2`
- `web/app/dist/assets/roboto-vietnamese-300-italic-B9qvi8_k.woff`
- `web/app/dist/assets/roboto-vietnamese-300-italic-CtUKlc4H.woff2`
- `web/app/dist/assets/roboto-vietnamese-300-normal-BPvXm_f1.woff2`
- `web/app/dist/assets/roboto-vietnamese-300-normal-INUupD3o.woff`
- `web/app/dist/assets/roboto-vietnamese-400-italic-C41J4i52.woff2`
- `web/app/dist/assets/roboto-vietnamese-400-italic-RF0eew8q.woff`
- `web/app/dist/assets/roboto-vietnamese-400-normal-D2PTxGxD.woff2`
- `web/app/dist/assets/roboto-vietnamese-400-normal-DnpnVwnf.woff`
- `web/app/dist/assets/roboto-vietnamese-500-italic-BddoBFd0.woff2`
- `web/app/dist/assets/roboto-vietnamese-500-italic-CmEfQ1B1.woff`
- `web/app/dist/assets/roboto-vietnamese-500-normal-B3ncpOoB.woff2`
- `web/app/dist/assets/roboto-vietnamese-500-normal-D380IkQ8.woff`
- `web/app/dist/assets/roboto-vietnamese-700-italic-By4b4cXv.woff`
- `web/app/dist/assets/roboto-vietnamese-700-italic-CVc74C74.woff2`
- `web/app/dist/assets/roboto-vietnamese-700-normal-BEVeWqJt.woff2`
- `web/app/dist/assets/roboto-vietnamese-700-normal-DsFyXAL4.woff`
- `web/app/dist/assets/roboto-vietnamese-900-italic-D0nMCc5h.woff`
- `web/app/dist/assets/roboto-vietnamese-900-italic-hVMOC7we.woff2`
- `web/app/dist/assets/roboto-vietnamese-900-normal-C48YQOjq.woff`
- `web/app/dist/assets/roboto-vietnamese-900-normal-D-H2ldSl.woff2`
- `web/app/dist/assets/ruby-DezsRK8O.js`
- `web/app/dist/assets/rust-DdL9SqIa.js`
- `web/app/dist/assets/sb-CcwsVR0C.js`
- `web/app/dist/assets/scala-DHpiXF5c.js`
- `web/app/dist/assets/scheme-BeGwcela.js`
- `web/app/dist/assets/scss-gp-XZpBa.js`
- `web/app/dist/assets/shell-CC2rA5mh.js`
- `web/app/dist/assets/solidity-BEEn4gHE.js`
- `web/app/dist/assets/sophia-CRfGWb83.js`
- `web/app/dist/assets/sparql-D_Lu-MrJ.js`
- `web/app/dist/assets/sql-NEE52Syq.js`
- `web/app/dist/assets/st-DbInun42.js`
- `web/app/dist/assets/status-CNSle5LJ.js`
- `web/app/dist/assets/swift-Bxkupp3x.js`
- `web/app/dist/assets/systemverilog-Bz4Y3fRF.js`
- `web/app/dist/assets/tcl-DISqw1ZD.js`
- `web/app/dist/assets/ts.worker-B6r0Skfj.js`
- `web/app/dist/assets/tsMode-B1_NfjHa.js`
- `web/app/dist/assets/twig-De2hgUGE.js`
- `web/app/dist/assets/typescript-B9Bb_0iF.js`
- `web/app/dist/assets/typespec-B8J7ngcE.js`
- `web/app/dist/assets/useServerList-CokRfazm.js`
- `web/app/dist/assets/useSnackbar-D7YOfKok.js`
- `web/app/dist/assets/validation-Yxuom3Du.js`
- `web/app/dist/assets/vb-DV3o63ZY.js`
- `web/app/dist/assets/wgsl-DpFanUEy.js`
- `web/app/dist/assets/workers-DLQCZ--h.js`
- `web/app/dist/assets/xml-BsJ5i10T.js`
- `web/app/dist/assets/yaml-DT_Rfd72.js`
- `web/app/dist/favicon.ico`
- `web/app/dist/index.html`
@@ -0,0 +1,148 @@
# Nexus Release Index — 2026-07-08
## Status
```text
local_artifact_ready=true
online_deployed=false
remote_ssh_blocked_by_codex_approval=true
```
Codex has not modified the production server in this phase. Remote SSH/network checks were blocked by the approval auto-review endpoint returning HTTP 404.
## Artifact
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256:
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
Standard checksum file:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz.sha256
```
Metadata:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.metadata.json
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-artifact-metadata-20260708.md
```
Current artifact facts:
```text
size_mb=27.02
entries=2409
files=2409
```
## Main documents
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-ready-checklist-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-manual-deploy-commands-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-app-v2-双后台实施说明-20260707.md
```
## One-command local preparation
Run from the source snapshot:
```cmd
.venv-py312-codex\Scripts\python.exe scripts\prepare_release_artifact.py --old-snapshot C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --output-dir C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs --name nexus-release-20260708
```
Verified result:
```text
[prepare-release-artifact] OK
added=381 modified=26 deleted=347
60 passed in 1.70s
OK: release bundle looks clean
Validate-only mode: tarball checksum and content validation passed
```
## Server-side apply command
Upload:
```cmd
scp C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz nexus:/tmp/nexus-release-20260708.tar.gz
```
If the new apply script is not yet on the server, extract it first:
```bash
cd /tmp
tar xzf nexus-release-20260708.tar.gz nexus-release/deploy/apply-release-bundle.sh
```
Validate only:
```bash
bash /tmp/nexus-release/deploy/apply-release-bundle.sh --validate-only \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
Apply:
```bash
sudo bash /tmp/nexus-release/deploy/apply-release-bundle.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
## Online verification after deploy
```cmd
.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip
```
Expected:
```text
OK: health
OK: /app
OK: /app-v2
OK: online release verification passed
```
## BtPanel verification
After deploy:
1. Existing `/app/` works.
2. New `/app-v2/` works.
3. BtPanel one-click login works.
4. Same server duplicate one-click login is serialized; conflict returns HTTP 409.
5. Newly added BtPanel servers get hardcoded_3600 detection/repair before one-click login.
6. Multiple BtPanel sessions remain logged in after 2 hours and refresh.
## Server preflight command
Before applying the release on the production host, run:
```bash
bash /tmp/nexus-release/deploy/preflight-release-host.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
This checks required commands, checksum, release tar paths, deploy path, runtime hints, disk space, and current local endpoints before applying changes.
- `Nexus-release-text-integrity-gate-20260708.md`:本轮新增文本完整性 Gate、文档控制字符修复和最新制品 SHA 记录。
- `Nexus-release-python-publisher-20260708.md`:Python 版发布辅助脚本说明,避免 PowerShell 管道/引号/编码问题。
- `Nexus-release-publisher-tests-20260708.md`:发布辅助脚本离线测试覆盖与 diff manifest 排除 tmp 修复记录。
@@ -0,0 +1,55 @@
# Nexus 发布辅助脚本测试覆盖记录(2026-07-08)
## 本轮目的
继续降低线上发布风险:为 `scripts/publish_release_bundle.py` 增加离线单元测试,并修复 diff manifest 对测试临时文件的误收录。
## 新增测试
文件:
```text
work\nexus_code_context\snapshots\nexus-source-20260707-112042\tests\test_publish_release_bundle.py
```
覆盖 5 个场景:
1. 默认模式:上传 + preflight,不执行 apply
2. 显式 `--apply`preflight 后才执行 `apply-release-bundle.sh`
3. `--no-upload --dry-run`:只生成远端 SSH 命令,不触发 scp;
4. SHA256 mismatch:在任何远端命令前失败;
5. 远端路径:始终使用 POSIX `/tmp/...`,避免 Windows `\tmp\...`
## Gate 更新
`run_local_release_gate.py --with-pytest` 现在会执行:
- 宝塔专项测试:60 个;
- 发布辅助脚本测试:5 个。
合计:65 个 Python 测试。
## Manifest 修复
`scripts/generate_release_diff.py` 新增排除 `tmp` 目录,避免测试临时 tarball 进入 diff manifest。
已确认:
- release bundle 不包含 `tmp/pytest-publish-release`
- diff manifest 不包含 `tmp/pytest-publish-release`
- package scan 通过;
- validate-only 通过。
## 最新发布包
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
entries`2409`
@@ -0,0 +1,81 @@
# Nexus Python 发布辅助脚本(2026-07-08
## 目的
为避免 Windows PowerShell 管道、编码和引号问题,新增 Python 发布辅助脚本:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\publish_release_bundle.py
```
它使用 `subprocess` 参数列表调用 `scp` / `ssh`,不依赖本地 shell 管道;默认只上传发布包并执行服务器侧 preflight,不会直接发布。
## 最新发布包
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
## 推荐使用方式
### 1. 只上传并执行 preflight(默认安全模式)
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
### 2. 如果不使用 SSH config
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote root@你的线上服务器IP ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^
--deploy-path /opt/nexus
```
### 3. preflight 确认无误后执行 apply
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^
--apply
```
## 行为说明
脚本会:
1. 本地校验 tarball SHA256
2. 使用 `scp` 上传到 `/tmp/nexus-release-20260708.tar.gz`
3. 在远端从发布包解出:
- `deploy/preflight-release-host.sh`
- `deploy/apply-release-bundle.sh`
4. 执行 preflight
5. 只有传入 `--apply` 时才执行正式 apply。
## 已验证
- `--help` 正常;
- `--dry-run --no-upload` 正常;
- Windows 下远端路径保持 POSIX `/tmp/...`,不会被转换成 `\tmp\...`
- 已纳入 `scripts/run_local_release_gate.py``py_compile`
## 本轮新增测试
- `tests/test_publish_release_bundle.py`:5 个离线测试,覆盖默认 preflight、显式 apply、SHA mismatch、no-upload dry-run、远端 POSIX 路径。
- 已纳入 `scripts/run_local_release_gate.py --with-pytest`
@@ -0,0 +1,495 @@
# Nexus 发布前清单(2026-07-08
## 结论
当前本地快照已经通过完整发布前 Gate:
```text
py_compile + /app-v2 build:safe + app-v2 sensitive scan + 宝塔 pytest
```
最终结果:
```text
[release-gate] OK
60 passed in 5.33s
```
## 当前源码快照
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
```
对比基准快照:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236
```
完整差异清单:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md
```
## 差异清单汇总
重新生成后的清单已经排除:
- `.git`
- `node_modules`
- `.venv-*`
- `venv`
- `__pycache__`
- `.ruff_cache`
- `.playwright-mcp`
- `.cursor`
- `.pytest_cache`
- `*.pyc`
- `*.pyo`
- `*.bak-`
- `2025.2`
- `=2025.2`
- `tmp/nexus-frontend.tar.gz`
当前差异统计:
```text
added=381
modified=26
deleted=347
```
注意:差异里包含大量旧 `/app/` 静态构建产物变动。如果只发布本轮宝塔稳定性与 `/app-v2`,可以优先发布“核心发布集合”,不要盲目同步整个快照目录。
## 核心发布集合
### 后端宝塔一键登录稳定性
```text
server/application/services/btpanel_service.py
server/infrastructure/btpanel/bootstrap_lock.py
server/infrastructure/btpanel/login_url_lock.py
server/infrastructure/btpanel/ssh_bootstrap.py
server/main.py
```
### API / 服务相关历史改动
差异清单中还有以下后端文件发生变化,发布前需要按实际范围确认是否一起发布:
```text
server/api/assets.py
server/api/scripts.py
server/api/sync_v2.py
server/application/services/server_file_transfer_service.py
server/infrastructure/ssh/remote_archive.py
server/utils/files_chmod_policy.py
```
这些文件属于此前安全巡检/文件传输/脚本执行相关改动,不是本轮宝塔一键登录最小集合;如果线上已经发布过,可跳过或只按 diff 覆盖。
### `/app-v2` 双后台
源码:
```text
frontend-v2/
```
构建产物:
```text
web/app-v2/
```
后端静态挂载:
```text
server/main.py
```
Docker
```text
Dockerfile
Dockerfile.prod
```
### 测试与发布脚本
```text
requirements-dev.txt
scripts/check_backend_test_env.py
scripts/generate_release_diff.py
scripts/run_btpanel_tests.py
scripts/run_btpanel_tests_docker.cmd
scripts/run_btpanel_tests_docker.sh
scripts/run_local_release_gate.py
scripts/scan_release_manifest.py
```
### 测试文件
```text
tests/test_btpanel_bootstrap_lock.py
tests/test_btpanel_get_config.py
tests/test_btpanel_login_url.py
tests/test_btpanel_login_url_route.py
tests/test_btpanel_ssh_bootstrap.py
```
## 发布前验证命令
### 1. 重新生成差异清单
```cmd
.venv-py312-codex\Scripts\python.exe scripts\generate_release_diff.py --old C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --new C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --output C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md
```
结果:
```text
added=369 modified=21 deleted=347
```
### 2. 扫描发布候选文件是否包含已知环境泄露标记
```cmd
.venv-py312-codex\Scripts\python.exe scripts\scan_release_manifest.py --root C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --manifest C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md
```
结果:
```text
OK: no known environment leak markers found in release manifest entries
```
### 3. 完整发布前 Gate
```cmd
.venv-py312-codex\Scripts\python.exe scripts\run_local_release_gate.py --with-pytest
```
结果:
```text
[app-v2-sensitive-scan] OK: scanned 9 file(s), no high-confidence secrets found
collected 60 items
60 passed in 1.79s
[release-gate] OK
```
最新 Gate 追加确认:
```text
bash -n deploy/sync_webapp_to_container.sh
60 passed in 1.70s
[release-gate] OK
```
### 4. 生成干净发布包
```cmd
.venv-py312-codex\Scripts\python.exe scripts\generate_release_bundle.py --root C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --output C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
结果:
```text
files=2409
```
发布包位置:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
发布包元数据:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.metadata.json
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-artifact-metadata-20260708.md
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
### 5. 扫描发布包
```cmd
.venv-py312-codex\Scripts\python.exe scripts\scan_release_bundle.py C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
结果:
```text
size_mb=27.02
entries=2409
OK: release bundle looks clean
```
### 6. 部署排除规则自检
`scripts/run_local_release_gate.py` 现在会自动执行:
```cmd
.venv-py312-codex\Scripts\python.exe scripts\check_deploy_excludes.py
```
结果:
```text
OK: deploy exclusion patterns are present
```
### 7. 发布后线上验证脚本
新增:
```text
scripts/verify_online_release.py
```
用途:
- 检查 `/health`
- 检查旧后台 `/app/`
- 检查新后台 `/app-v2/`
- 检查页面里不要出现 `全局 API Key``Global API Key``tmp_login``tmp_token`
发布后执行:
```cmd
.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip
```
## 明确不发布
以下内容不要同步到正式服务器:
```text
.venv-py312-codex/
.venv-py314/
venv/
frontend-v2/node_modules/
.pytest_cache/
__pycache__/
.ruff_cache/
.cursor/
.playwright-mcp/
frontend-v2/node_modules/
*.pyc
*.pyo
*.bak-
2025.2
=2025.2
tmp/nexus-frontend.tar.gz
```
其中 `2025.2` / `=2025.2` 是 Windows CMD 执行 `tzdata>=2025.2` 时可能产生的重定向误文件,绝对不要发布。
## 建议发布顺序
### 方案 A:Docker 镜像重建发布(推荐)
1. 使用当前快照作为构建上下文,但确保 `.dockerignore` 排除虚拟环境、node_modules、缓存和临时文件。
2. 构建生产镜像。
3. 启动新容器前备份旧容器镜像/tag 和数据库。
4. 发布后执行线上验证。
优点:静态资源、后端代码、依赖环境一致性更好。
当前已修复:
- `.dockerignore` 已排除 `frontend-v2/node_modules``.venv-*``.pytest_cache``2025.2``*.bak-` 等。
- `deploy/rsync-local-to-server.sh` 已排除 `frontend-v2/node_modules``.venv-*``.pytest_cache``2025.2``*.bak-` 等。
- `scripts/run_local_release_gate.py` 已增加 `bash -n` 检查 `deploy/deploy-production.sh``deploy/rsync-local-to-server.sh`
- `deploy/deploy-production.sh` 发布后健康检查已增加 `/app-v2/`
- `deploy/deploy-production.sh` 已显式构建并上传 `web/app-v2`,不只依赖 Dockerfile 构建。
- `deploy/apply-release-bundle.sh` 已新增,可在服务器侧对 tar.gz 发布包执行 SHA256 校验、备份、解包、重建和本机验证。
- `deploy/sync_webapp_to_container.sh` 已同步 `web/app-v2` 到容器,并校验 `/app-v2/` 主 bundle。
- `scripts/run_local_release_gate.py` 已增加 `bash -n deploy/sync_webapp_to_container.sh`
- `scripts/check_deploy_excludes.py` 会防止发布排除规则未来被误删。
### 当前远程执行状态
本轮尝试对线上 `Host nexus` 做只读 SSH 探测时,Codex 审批系统返回 404 并拒绝执行。不是 SSH 凭据或代码错误,而是自动审批代理不可用。
因此本轮没有实际修改线上服务器。当前状态是:
```text
本地发布包:已生成
发布包扫描:通过
完整 release gate:通过
线上部署:尚未执行
```
如果要由 Codex 继续实际发布,需要在下一步明确允许 SSH 发布操作。
## 手动发布命令清单
已生成:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-manual-deploy-commands-20260708.md
```
该文档包含:
- `deploy/deploy-production.sh` 一键发布方式。
- tar.gz 发布包上传方式。
- 线上 SHA256 校验。
- 线上备份、解包、Docker 重建。
- 发布后 `/health``/app/``/app-v2/` 验证。
- 宝塔专项验证。
- 回滚命令。
## 最新最终验证
发布包扫描:
```text
size_mb=27.02
entries=2409
OK: release bundle looks clean
```
完整 Gate
```text
60 passed in 1.70s
[release-gate] OK
```
### 方案 B:只同步核心文件到现有线上目录
适合快速热修:
1. 同步后端核心文件。
2. 同步 `web/app-v2/`
3. 重启后端服务。
4. 验证宝塔一键登录和 `/app-v2/`
缺点:如果线上依赖、静态目录或 Dockerfile 状态与本地不一致,容易遗漏。
## 线上验证清单
发布到:
```text
https://api.synaglobal.vip
```
后验证:
1. `/app/` 旧后台仍可访问。
2. `/app-v2/` 新后台可访问。
3. `/api/btpanel/servers/{id}/login-url` 正常返回登录地址。
4. 同一台宝塔短时间重复点一键登录,应该只允许一个签发流程,冲突时返回 HTTP 409。
5. 新增宝塔或配置缺失宝塔,在一键登录前会触发 session cleanup TTL 兜底检测/修复。
6. 多台宝塔同时打开,2 小时后刷新不应因为 `task.py hardcoded_3600` 陆续掉线。
7. 线上页面不显示全局 API Key 明文。
## 回滚点
发布前建议备份:
```text
server/application/services/btpanel_service.py
server/infrastructure/btpanel/bootstrap_lock.py
server/infrastructure/btpanel/login_url_lock.py
server/infrastructure/btpanel/ssh_bootstrap.py
server/main.py
web/app-v2/
Dockerfile
Dockerfile.prod
```
如果发布后宝塔一键登录异常:
1. 先回滚上述后端文件。
2. 重启后端服务。
3. 保留 `/app-v2/` 不影响旧 `/app/`,必要时可以只移除 `web/app-v2/` 静态目录或取消 `server/main.py` 挂载。
## 2026-07-08 validate-only verification
The server-side release applier now supports validate-only mode.
Command verified locally with Git Bash:
```bash
bash deploy/apply-release-bundle.sh --validate-only --tarball /c/Users/uzuma/Documents/Codex/2026-07-06/yuu/outputs/nexus-release-20260708.tar.gz --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
`
Result:
` ext
SHA256 OK: 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
Extracted release tree validation OK
Validate-only mode: tarball checksum and content validation passed
`
Network/SSH production execution is still not performed because Codex approval auto-review returned HTTP 404 for read-only checks.
## 2026-07-08 one-command local artifact preparation
A single local command now prepares and verifies the release artifact end-to-end:
`cmd
.venv-py312-codex\Scripts\python.exe scripts\prepare_release_artifact.py --old-snapshot C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --output-dir C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs --name nexus-release-20260708
`
Verified result:
` ext
[prepare-release-artifact] OK
added=381 modified=26 deleted=347
files=2409
entries=2409
size_mb=27.02
sha256=46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
60 passed in 1.70s
`
## 2026-07-08 release index and checksum file
Release index:
` ext
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-index-20260708.md
`
Standard SHA256 checksum file:
` ext
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz.sha256
`
The checksum file contains:
` ext
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b nexus-release-20260708.tar.gz
`
## Server preflight command
Before applying the release on the production host, run:
```bash
bash /tmp/nexus-release/deploy/preflight-release-host.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
This checks required commands, checksum, release tar paths, deploy path, runtime hints, disk space, and current local endpoints before applying changes.
@@ -0,0 +1,52 @@
# Nexus 发布文本完整性 Gate 增量记录(2026-07-08
## 背景
本轮在发布文档中发现隐藏控制字符 `0x08`,会把 Markdown 代码块显示成异常的 `bash` fence。该问题容易由 Windows PowerShell/终端编码或临时脚本拼接引入,肉眼不容易发现。
## 已修复
1. 新增源码检查脚本:
- `scripts/check_text_integrity.py`
2. 接入本地发布 Gate
- `scripts/run_local_release_gate.py`
3. 清理文档隐藏字符:
- `outputs/*.md` 中的 `0x08` 已清除。
- `docs/research/jumpserver-easynode-architecture-analysis.md` 中真实 ANSI ESC 已替换为可读的 `\u001b`
4. 重新生成发布包:
- `nexus-release-20260708.tar.gz`
- SHA256: `46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b`
- entries: `2407`
## Gate 覆盖
`check_text_integrity.py` 会检查:
- UTF-8 解码异常;
- 隐藏控制字符,允许 `\n``\r``\t`
- 典型坏 fence:真实 backspace 混入 bash 代码块;
- 自动跳过旧后台构建产物和 vendor 目录,避免误报 xterm/Monaco 的合法控制码表。
## 验证结果
- 文本完整性检查:`OK: scanned 1751 text file(s)`
- `/app-v2` build:safe:通过
- app-v2 sensitive scan:通过
- 宝塔专项 pytest`60 passed`
- 发布包扫描:通过
- 发布包 validate-only:通过
- metadata / `.sha256` / 实际 tar SHA:一致
## 当前最新发布包
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b nexus-release-20260708.tar.gz
```
## 线上状态
截至本记录生成时,线上 `https://api.synaglobal.vip` 尚未由 Codex 完成发布验证;原因仍是本机 Codex 审批代理 `codex-auto-review` 不可用导致联网/SSH 操作被拒绝。
@@ -0,0 +1,174 @@
# Nexus 代码上下文库与局域网 Git 基线报告
生成时间:2026-07-07 07:18Asia/Shanghai
## 1. 当前结论
已经完成两件事:
1. 在局域网 Linux 测试机 `/opt/nexus-dev-current` 初始化了本地 Git 仓库。
2. 在本地 Codex 工作区生成了 Nexus JSONL 代码上下文库和第一版安全审查入口报告。
没有使用 SQLite。
## 2. 局域网 Linux Git 状态
测试机:`192.168.124.219`
Nexus 目录:
```text
/opt/nexus-dev-current -> /opt/nexus-dev-20260707-054832
```
Git 仓库实际位置:
```text
/opt/nexus-dev-20260707-054832/.git
```
基线提交:
```text
9d42fcd chore: baseline Nexus LAN test deployment snapshot
```
当前分支:
```text
audit/security-review
```
当前状态:
```text
clean
```
## 3. Git 防误提交保护
已配置 `.git/info/exclude` 本地排除规则,防止真实运行 secrets 和本地凭据误入库。
已排除重点包括:
```text
.env
.env.*
docker/.env
docker/.env.*
*.pem
*.key
*.p12
*.pfx
*resolved*
*凭据*
机器与凭据.md
ssh-config.resolved.txt
nexus-1panel.secrets.resolved.sh
nexus-1panel.secrets.sh
SECRETS.md
.git-askpass-nexus.sh
scripts/npm-proxy.env
*.bak
*.bak-*
node_modules/
.venv/
venv/
__pycache__/
.pytest_cache/
```
提交前已检查高危敏感文件名,没有发现 `.env`、pem/key、resolved secrets 等被跟踪。
## 4. 本地代码上下文库
本地源代码快照:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236
```
本地上下文库:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context
```
输出副本:
```text
outputs/nexus-code-context-20260707
```
## 5. 索引统计
| 项 | 数量 |
|---|---:|
| files | 2577 |
| text_files | 2103 |
| symbols | 2823 |
| api_routes | 214 |
| service_methods | 319 |
| call_edges | 16594 |
| db_queries | 433 |
| redis_keys | 725 |
| ssh_commands | 343 |
| btpanel_flows | 172 |
| frontend_routes | 31 |
| secret_refs | 4425 |
| grep_hits | 8533 |
| chunks | 1312 |
## 6. 已生成的核心报告
```text
reports/00-overview.md
reports/01-api-routes.md
reports/02-service-methods.md
reports/03-btpanel-login-chain.md
reports/04-ssh-risk-map.md
reports/05-redis-lock-map.md
reports/06-db-query-map.md
reports/07-security-review-entrypoints.md
```
## 7. 已生成的 JSONL 索引
```text
index/files.jsonl
index/symbols.jsonl
index/api_routes.jsonl
index/service_methods.jsonl
index/call_edges.jsonl
index/db_queries.jsonl
index/redis_keys.jsonl
index/ssh_commands.jsonl
index/btpanel_flows.jsonl
index/frontend_routes.jsonl
index/secrets_inventory.jsonl
```
## 8. 后续建议
下一步建议按这个顺序开始真正安全审查:
1. 先审 `03-btpanel-login-chain.md`,确认宝塔一键登录链路和长期掉线边界。
2. 再审 `04-ssh-risk-map.md`,确认 SSH/远程命令拼接、quote、timeout、日志脱敏。
3. 再审 `01-api-routes.md``07-security-review-entrypoints.md`,确认敏感 API 是否都有鉴权。
4. 最后按 `05-redis-lock-map.md``06-db-query-map.md` 审 Redis 锁/TTL 和 DB 查询。
后续所有代码修改建议在测试机分支执行:
```bash
git status
git diff
git add <files>
git commit -m "fix: ..."
```
修复后继续跑:
```bash
python -m pytest -q
```
@@ -0,0 +1,368 @@
# Nexus 安全巡检阶段 4Settings/Auth 与宝塔长期会话边界审查
时间:2026-07-07
对象:测试机 `/opt/nexus-dev-current`,分支 `audit/security-review`
最新提交:`524acd5 fix-stop-exposing-global-api-key-in-settings`
> 本报告不输出任何密码、API Key、Token、Cookie、PEM 内容。
---
## 1. 本轮确认结果
### 1.1 全局 API Key 不显示:已完成代码层修复
只读检查结果:
- 后端 `/api/settings/api-key/reveal` 已删除。
- 前端 SettingsPage 中全局 API Key 显示/复制逻辑已删除。
- 源码残留仅有:
- `server/api/servers.py` 两处注释仍提到旧名字 `reveal_api_key`
- `frontend/src/utils/auditLabels.ts` 仍有旧审计 label `reveal_api_key: 查看 API 密钥`
安全判断:
- 残留注释/label 不会造成 API Key 暴露。
- 建议后续做一次小清理,避免误导维护者和审计日志翻译。
### 1.2 当前代码没有全局 API 限速
沿用上一份报告结论:
- 没有 slowapi / fastapi-limiter / nginx limit_req 一类全局限速。
- 429 主要来自:
- 管理员登录失败锁定:5 次 / 15 分钟。
- Agent 脚本回调:job 10 次 / 60 秒,IP 60 次 / 60 秒。
- 大多数“速度限制”是远程任务并发保护,不是 HTTP API 限速。
---
## 2. 宝塔 2 小时后刷新掉线:当前代码仍有一个关键边界
### 2.1 当前一键登录链路
```mermaid
sequenceDiagram
participant UI as Nexus 前端
participant API as /api/btpanel/servers/{id}/login-url
participant SVC as BtPanelService.create_login_url
participant Redis as Redis login_url_lock
participant SSH as ssh_bootstrap_panel
participant BT as 宝塔面板
UI->>API: 请求一键登录 URL
API->>SVC: create_login_url(server_id)
SVC->>Redis: 获取每台服务器分布式锁
SVC->>SSH: best-effort 检查/patch 宝塔 task.py session cleanup
SVC->>BT: config?action=set_temp_login(expire_time=now+86400)
BT-->>SVC: 返回 tmp_token 或登录 URL
SVC-->>UI: /login?tmp_token=...
```
### 2.2 已修复的问题
当前代码已经解决:
1. `tmp_token` 使用 86400 秒 TTL,而不是宝塔默认较短 TTL。
2. 登录 URL 不缓存,因为 `tmp_token` 是一次性链接。
3. 对同一台服务器生成登录 URL 时使用 Redis 锁,避免并发覆盖。
4. 登录前会 best-effort patch 宝塔 `/www/server/panel/task.py`:把硬编码 `if f_time > 3600:` 改成读取 `public.get_session_timeout() or 86400`
### 2.3 仍可能导致“2 小时后刷新掉线”的关键点
当前 patch 的语义是:
```python
session_cleanup_timeout = int(public.get_session_timeout() or 86400)
if f_time > session_cleanup_timeout:
删除 session 文件
```
这意味着:
- 如果宝塔面板里已经配置了 session timeout,例如 7200 秒(2 小时),patch 会尊重这个值。
- `or 86400` 只在没有配置值时生效。
- 所以用户遇到“打开 10 个宝塔,2 小时后陆续刷新掉线”,仍可能是目标宝塔的 `session_timeout` 本身就是 7200 秒。
结论:
> 现在的修复主要解决 `task.py` 硬编码 3600 秒清理问题,但不一定强制把宝塔会话生命周期提升到 24 小时。如果目标面板配置仍是 2 小时,会继续 2 小时后掉线。
### 2.4 并发打开 10 个宝塔的边界
当前 Redis 锁可以保证同一台服务器不会同时生成多个互相覆盖的 `tmp_token`。但用户说的是“同时打开 10 个不同宝塔”,这属于不同 server_id:
- 每台服务器独立锁,不互相等待。
- 各自的 session 生命周期取决于各自宝塔面板配置和 task.py patch 状态。
- 10 台里有的先掉、有的后掉,符合“每台服务器 session_timeout/task.py/定时清理状态不一致”的表现。
---
## 3. Settings/Auth 发现的问题与建议
### 3.1 中风险:Telegram Token reveal 不需要二次认证
位置:
- `server/api/settings.py` `/telegram/reveal-token`
- `server/api/settings.py` `/telegram/offline/reveal-token`
现状:
- 需要 JWT。
- 会写审计日志。
- 但不像密码预设 reveal、SSH Key reveal 那样要求当前密码二次认证。
风险:
- 如果管理员 access token 被盗,在 token 有效期内可直接读取 Telegram Bot Token。
- 虽然这是管理员功能,但与同文件中“密码预设/SSH 私钥 reveal 需要二次认证”的策略不一致。
建议:
- 若继续允许显示 Telegram Token,建议改成 `current_password` 二次认证。
- 如果你倾向于“全局 API Key 一样不显示”,也可以直接删除 Telegram Token reveal,只允许覆盖写入。
### 3.2 中风险:TOTP setup/enable 不要求当前密码
位置:
- `server/api/auth.py` `/totp/setup`
- `server/api/auth.py` `/totp/enable`
现状:
- 需要 JWT。
- 只允许对自己操作。
- enable 前会校验 TOTP code。
- 但不要求当前密码。
风险:
- 如果 access token 被盗,攻击者可以生成自己的 TOTP secret 并启用,造成账号被锁或持久化控制风险。
建议:
- setup 或 enable 至少其中一步要求当前密码。
- 更严格方案:setup、enable、disable、password change 都统一走“敏感操作二次认证”。
### 3.3 低风险:全局 API Key 相关审计 label/注释残留
位置:
- `server/api/servers.py` 注释里仍有 `same pattern as reveal_api_key`
- `frontend/src/utils/auditLabels.ts` 仍有 `reveal_api_key: 查看 API 密钥`
风险:
- 不暴露 secret。
- 但会误导后续开发,以为还有 reveal_api_key 功能。
建议:
- 后续清理为通用描述,例如 `same pattern as sensitive operation re-auth`
- 删除前端旧 label 或保留但标注 legacy。
### 3.4 低风险:refresh 接口仍接受 body refresh_token fallback
位置:
- `server/api/auth.py` `/api/auth/refresh`
现状:
- 优先读取 HttpOnly cookie。
- 没有 cookie 时允许从 body 读取 refresh_token。
风险:
- 当前注释解释为兼容 fallback。
- 如果没有明确的历史客户端需要,body fallback 增加了一条 token 传输路径。
建议:
- 保守:保留但加审计/废弃期。
- 严格:只允许 HttpOnly cookie refresh。
---
## 4. 下一步修改方向:至少 6 个方案供选择
> 按你的要求,涉及安全策略/代码改动前先给多个选项,不直接替你定。
### 方案 A:只修宝塔长期会话,强制把宝塔 session timeout 提升到 24 小时
改动:
- 在 `ssh_bootstrap.py` 里增加写入/修正宝塔 session timeout 配置的逻辑。
- 登录前 best-effort 确保目标面板的 session timeout >= 86400。
- 保留现有 task.py patch。
好处:
- 最直接解决“2 小时后刷新掉线”。
- 不影响 Nexus 自身安全策略。
风险:
- 会修改宝塔面板自身配置。
- 如果用户希望宝塔短会话,这会改变预期。
是否需要重启/部署:
- 需要重建/重启 Nexus 后端容器。
- 对目标宝塔可能需要 reload bt。
推荐度:高,针对当前痛点最直接。
### 方案 B:宝塔 session timeout 只做检测和告警,不自动修改
改动:
- Nexus 登录前 SSH 检测宝塔当前 session timeout。
- 如果低于 86400,在 UI 或接口响应中返回 warning。
- 不自动改宝塔。
好处:
- 风险低,不擅自改目标面板。
- 可解释每台为什么 2 小时掉。
风险:
- 不自动解决问题,仍需要人工确认/点击修复。
推荐度:中。
### 方案 C:给宝塔登录 URL 接口增加“会话保活/会话诊断”返回字段
改动:
- `/api/btpanel/servers/{id}/login-url` 返回:
- `session_cleanup_ttl_ok`
- `session_cleanup_ttl_patched`
- `session_timeout_seconds`
- `warnings`
- 前端一键登录时提示“当前宝塔会话约 2 小时后失效”。
好处:
- 用户能看到问题原因。
- 便于排查 10 台里哪台会掉。
风险:
- 需要补前端显示。
- 只诊断,不一定修复。
推荐度:中高,适合和 A/B 搭配。
### 方案 D:敏感 Token 全部不显示,只允许覆盖写入
改动:
- 删除 Telegram Token reveal。
- 保留 token 是否已设置状态。
- 修改前端:显示“已设置,留空不改”,不提供查看/复制。
好处:
- 和“全局 API Key 不显示”策略一致。
- 降低 access token 被盗后的进一步泄露面。
风险:
- 管理员无法从 Nexus 找回 Telegram Token,只能重填。
推荐度:中高,安全更干净。
### 方案 E:保留 Telegram/密码/SSH reveal,但统一要求二次认证
改动:
- Telegram reveal 请求体增加 `current_password`
- 前端弹窗输入当前密码。
- 可以同时把 reveal 审计统一标准化。
好处:
- 兼顾可用性和安全。
- 与密码预设、SSH Key reveal 保持一致。
风险:
- 使用上多一步。
推荐度:高,如果你还想保留“查看 Telegram Token”。
### 方案 F:统一敏感操作二次认证框架
改动范围更大:
- TOTP setup/enable/disable。
- 修改密码。
- reveal 密码预设/SSH Key/Telegram Token。
- 宝塔 API Key 修改/显示类操作。
- 服务器凭据 reveal/更新。
好处:
- 安全策略统一,不会每个接口散落实现。
- 后续审计更清晰。
风险:
- 改动较大,测试量更大。
- 前端交互也要统一。
推荐度:长期推荐,短期可分阶段做。
### 方案 Grefresh token 只允许 HttpOnly Cookie,不再接受 body fallback
改动:
- `/api/auth/refresh` 删除 body refresh_token fallback。
- 如有历史客户端,先加配置开关或废弃日志。
好处:
- Token 传输路径更少。
- 更符合当前注释里的安全目标。
风险:
- 如果有脚本/旧前端依赖 body refresh,会受影响。
推荐度:中,先确认没有兼容需求再做。
---
## 5. 我建议的执行顺序
如果目标是先解决“宝塔 2 小时掉线”,建议:
1. 先做方案 C 的诊断字段或 SSH 检测脚本,确认目标面板 session timeout 是否为 7200。
2. 再做方案 A:自动把宝塔 session timeout 提升到 86400。
3. 回归测试:同一台宝塔一键登录后刷新不掉;多台并发生成登录 URL 不互相覆盖。
4. 然后再做 settings/auth 安全收口:优先方案 D 或 E。
如果目标是先收安全面,建议:
1. 方案 D 或 E 处理 Telegram Token。
2. 方案 F 分阶段统一敏感操作二次认证。
3. 方案 G 作为兼容性确认后的增强项。
---
## 6. 待用户选择
请在以下方向里选一个或多个:
1. A:自动强制宝塔 session timeout >= 86400,优先解决掉线。
2. B:只检测告警,不自动改宝塔。
3. C:一键登录返回会话诊断字段,前端提示原因。
4. DTelegram Token 像全局 API Key 一样完全不显示。
5. ETelegram Token 继续可显示,但必须当前密码二次认证。
6. F:做统一敏感操作二次认证框架。
7. Grefresh token 只允许 HttpOnly Cookie。
@@ -0,0 +1,224 @@
# Nexus 宝塔 Session 自动修复正式发布记录
日期:2026-07-07
正式域名:`https://api.synaglobal.vip`
正式主机:`20.24.218.235`
正式容器:`nexus-prod-nexus-1`
测试提交:`5ac3f56 fix: auto repair btpanel session cleanup ttl`
## 发布目标
把“新增宝塔自动检测/修复 hardcoded_3600”与“一键登录前兜底检测/修复”发布到正式环境,解决宝塔 tmp_login session 被 `/www/server/panel/task.py` 固定 3600 秒清理导致一键登录后 1~2 小时刷新掉线的问题。
## 本次正式发布范围
发布方式:`docker cp` 热更新正式容器内文件,然后重启容器。
说明:正式机 `/opt/nexus` 工作树较脏,本次没有执行 `git reset` / `git pull`,避免覆盖线上已有改动。
覆盖正式容器内文件:
- `/app/server/infrastructure/btpanel/ssh_bootstrap.py`
- `/app/server/infrastructure/btpanel/bootstrap_state.py`
- `/app/server/application/services/btpanel_service.py`
来源文件本地留存:
- `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\prod-release-20260707-bt-session\ssh_bootstrap.py`
- `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\prod-release-20260707-bt-session\bootstrap_state.py`
- `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\prod-release-20260707-bt-session\btpanel_service.py`
正式机 staging 目录:
- `/tmp/nexus_bt_session_release_20260707/`
正式机备份目录:
- `/tmp/nexus_bt_session_backup_20260707_194326/`
## 已发布能力
### 1. 新增宝塔 / bootstrap 阶段自动修复
在远程宝塔服务器上检查:
- `/www/server/panel/task.py`
如果发现旧版精确模式:
```python
if f_time > 3600:
```
则自动执行:
1. 备份原文件为 `/www/server/panel/task.py.nexus-session-ttl.bak`
2. 改为读取宝塔自身 session timeout 配置:
```python
session_cleanup_timeout = int(public.get_session_timeout() or 86400)
if f_time > session_cleanup_timeout:
```
3. 保留原文件权限;
4. 写入前执行 `compile(...)``py_compile.compile(..., doraise=True)`
5. 通过临时文件 + `os.replace(...)` 原子替换;
6. 变更后执行 `/etc/init.d/bt reload`
如果 task.py 结构不是已知安全模式,则记录 `unknown_task_pattern`,不做模糊替换,避免误改宝塔代码。
### 2. 一键登录前兜底检测/修复
`BtPanelService.create_login_url()` 生成一键登录 URL 前会检查已配置宝塔 API 的服务器:
- 成功修复/确认后:24 小时内不重复检查;
- 失败后:1 小时后允许重试;
- 兜底检查失败不会阻断本次一键登录,只会记录 diagnostics 和状态字段。
### 3. 状态字段
状态写入:`servers.extra_attrs.bt_panel`
新增/更新字段:
- `session_cleanup_ttl_ok`
- `session_cleanup_ttl_patched`
- `session_cleanup_task_state`
- `session_cleanup_ttl_checked_at`
- `session_cleanup_ttl_last_error`
- `panel_ssl`
`session_cleanup_task_state` 常见值:
- `patched_timeout`:已修好或本次修好;
- `unknown_task_pattern`:存在 task.py,但结构不是可安全自动 patch 的已知模式;
- `no_panel`:未找到宝塔面板 task.py
- `error`:检测/修复异常。
一键登录响应 diagnostics 会返回非敏感诊断字段,例如:
```json
{
"session_cleanup_ttl_ok": true,
"session_cleanup_ttl_patched": true,
"session_cleanup_task_state": "patched_timeout"
}
```
不会输出宝塔临时登录 token。
## 验证记录
### 测试部署机验证
测试部署机:`192.168.124.219`
项目目录:`/opt/nexus-dev-current`
分支:`audit/security-review`
执行测试:
```bash
.venv/bin/python -m pytest \
tests/test_btpanel_ssh_bootstrap.py \
tests/test_btpanel_login_url.py \
tests/test_btpanel_bootstrap_loop.py \
tests/test_btpanel_login_url_route.py \
tests/test_btpanel_ssh_login.py \
tests/test_btpanel_temp_login_ttl.py -q
```
结果:
```text
39 passed in 0.61s
```
### 正式容器发布前检查
正式容器临时目录内 3 个 Python 文件编译通过:
```bash
python -m py_compile \
/tmp/nexus_bt_session_release_20260707/ssh_bootstrap.py \
/tmp/nexus_bt_session_release_20260707/bootstrap_state.py \
/tmp/nexus_bt_session_release_20260707/btpanel_service.py
```
### 正式容器覆盖后检查
覆盖到 `/app/server/...` 后再次编译通过:
```bash
python -m py_compile \
/app/server/infrastructure/btpanel/ssh_bootstrap.py \
/app/server/infrastructure/btpanel/bootstrap_state.py \
/app/server/application/services/btpanel_service.py
```
代码关键字确认已生效:
- `unknown_task_pattern`
- `py_compile.compile(tmp, doraise=True)`
- `session_cleanup_task_state`
### 正式健康检查
正式容器重启后:
```text
nexus-prod-nexus-1 Up / healthy
http://127.0.0.1:8600/health => ok
https://api.synaglobal.vip/health => ok
```
## 安全边界
本次没有做以下高风险动作:
- 不显示、不保存、不输出宝塔 `tmp_token`
- 不输出 API Key、JWT、Cookie、密码、私钥;
- 不自动关闭宝塔 `check_client_hash()`
- 不自动修改宝塔面板 SSL 开关;
- 不对未知 task.py 结构做模糊替换;
- 不对正式机代码仓库做 `git reset` / `git pull`
- 不批量重启用户业务服务器,只在需要 patch 宝塔 task.py 时 reload 宝塔面板。
## 回滚方式
如需回滚本次正式热更新,可在正式主机执行以下思路:
```bash
# 1. 从备份目录恢复 3 个文件到容器
sudo docker cp /tmp/nexus_bt_session_backup_20260707_194326/ssh_bootstrap.py \
nexus-prod-nexus-1:/app/server/infrastructure/btpanel/ssh_bootstrap.py
sudo docker cp /tmp/nexus_bt_session_backup_20260707_194326/bootstrap_state.py \
nexus-prod-nexus-1:/app/server/infrastructure/btpanel/bootstrap_state.py
sudo docker cp /tmp/nexus_bt_session_backup_20260707_194326/btpanel_service.py \
nexus-prod-nexus-1:/app/server/application/services/btpanel_service.py
# 2. 编译检查
sudo docker exec nexus-prod-nexus-1 python -m py_compile \
/app/server/infrastructure/btpanel/ssh_bootstrap.py \
/app/server/infrastructure/btpanel/bootstrap_state.py \
/app/server/application/services/btpanel_service.py
# 3. 重启并检查健康
sudo docker restart nexus-prod-nexus-1
curl -sf http://127.0.0.1:8600/health
```
## 后续建议
1. 后续新增宝塔:bootstrap 阶段会自动检测/修复 hardcoded_3600。
2. 老的已绑定宝塔:用户点击一键登录前会兜底检测/修复。
3. 如果某台机器 diagnostics 显示 `unknown_task_pattern`,需要单独人工看那台宝塔的 `/www/server/panel/task.py`,不要自动硬替换。
4. 如果用户仍反馈“刷新后掉线”,优先检查该服务器的 diagnostics
- `session_cleanup_task_state`
- `session_cleanup_ttl_ok`
- `session_cleanup_ttl_last_error`
- 面板 SSL / 浏览器混合内容 / Cookie SameSite 等。
## 结论
正式环境 `https://api.synaglobal.vip` 已发布“新增宝塔自动检测/修复 hardcoded_3600”和“一键登录前兜底检测/修复”。当前正式健康检查通过,测试机相关用例全部通过。后续新增宝塔和老宝塔的一键登录入口都会自动进入这套检测/修复流程。
@@ -0,0 +1,132 @@
# Nexus 宝塔 `hardcoded_3600` 批量修复完成记录(2026-07-07)
## 修复范围
来源:`work/bt_bulk_session_audit.before_patch_20260707-1838.jsonl`
本次只处理:
- 在线服务器
- `task_state=hardcoded_3600`
- 共 148 台
本次没有处理:
- `panel_ssl=0` 的 112 台
- `unknown_task_pattern` 的 268 台
- 其它异常/无面板机器
## 修复动作
对每台目标服务器执行:
1. 检查 `/www/server/panel/task.py`
2. 查找硬编码逻辑:
- `if f_time > 3600:`
3. 在 session 清理循环前插入:
- 默认 `session_cleanup_timeout = 86400`
- 优先读取 `public.get_session_timeout()`
4. 把条件改成:
- `if f_time > session_cleanup_timeout:`
5. 写入前做备份:
- `/www/server/panel/task.py.nexus-session-ttl.bak`
6. 写入临时文件并执行 Python 编译检查。
7. 原子替换 `task.py`
8. 如果本次确实发生修改,则执行 `/etc/init.d/bt reload`
## 执行批次
第一批 10 台:执行 ID `5944`
剩余 138 台按 20 台一批执行:
| 批次 | 执行 ID | 数量 | 结果 |
|---:|---:|---:|---|
| 1 | 5944 | 10 | 成功 |
| 2 | 5945 | 20 | 成功 |
| 3 | 5946 | 20 | 成功 |
| 4 | 5947 | 20 | 成功 |
| 5 | 5948 | 20 | 成功 |
| 6 | 5949 | 20 | 成功 |
| 7 | 5950 | 20 | 成功 |
| 8 | 5951 | 18 | 成功 |
总计:148 / 148 成功。
## 执行结果
所有目标机器返回:
- `exit_code=0`
- `patch_result=patched`
- `after_patched=True`
本地结果文件:
- `work/bt_session_ttl_patch_batch_20260707-183535.json`
- `work/bt_session_ttl_patch_batch_20260707-183648_exec5945.json`
- `work/bt_session_ttl_patch_batch_20260707-183658_exec5946.json`
- `work/bt_session_ttl_patch_batch_20260707-183707_exec5947.json`
- `work/bt_session_ttl_patch_batch_20260707-183717_exec5948.json`
- `work/bt_session_ttl_patch_batch_20260707-183727_exec5949.json`
- `work/bt_session_ttl_patch_batch_20260707-183737_exec5950.json`
- `work/bt_session_ttl_patch_batch_20260707-183747_exec5951.json`
- `work/bt_session_ttl_patch_summary_20260707-183747.json`
## 修复后全量只读复查
复查范围:在线 420 台。
复查结果:
| 状态 | 数量 |
|---|---:|
| `patched_timeout` | 149 |
| `unknown_task_pattern` | 268 |
| `no_panel` | 1 |
| `error` | 2 |
| `hardcoded_3600` | 0 |
关键验证:
- 修复前 `hardcoded_3600`148 台
- 修复后这 148 台全部变为:`patched_timeout`
- 修复后剩余 `hardcoded_3600`0 台
全量复查中有 2 台轮询超时/解析异常:
- server_id `225`
- server_id `1`
这 2 台不是本轮 `hardcoded_3600` 未修复目标;本轮 148 台目标已全部验证成功。
## 当前剩余问题
仍有 112 台 `panel_ssl=0`,它们主要是另一类问题:
- 多数为 Ubuntu 24.04.4
- 多数为腾讯云 `VM-*` 主机名
- 多数属于 `unknown_task_pattern`
- 不适合直接套本轮 `hardcoded_3600` 补丁
建议下一步:对 112 台 `panel_ssl=0` 抽样 5-10 台读取 `task.py` / `public.py`,按宝塔版本分类后再决定策略。
## 回滚方式
单台回滚:
```bash
cp -a /www/server/panel/task.py.nexus-session-ttl.bak /www/server/panel/task.py
python -m py_compile /www/server/panel/task.py
/etc/init.d/bt reload
```
如果宝塔使用独立 pyenv,编译命令可替换为:
```bash
/www/server/panel/pyenv/bin/python3 -m py_compile /www/server/panel/task.py
```
## 结论
148 台 `hardcoded_3600` 已全部修复并通过复查,当前在线 420 台里 `hardcoded_3600` 已清零。
@@ -0,0 +1,403 @@
# Nexus 宝塔一键登录 2 小时掉线阶段诊断报告(2026-07-07)
## 1. 本轮结论
针对服务器 `42.193.182.190`Nexus server_id 约为 `395`)排查后,目前结论是:
1. **Nexus 生成的一键登录临时 token TTL 是 24 小时,不是 2 小时。**
2. 目标宝塔最近生成的 `temp_login` 记录 `ttl_seconds``86399/86400` 秒。
3. 宝塔面板登录超时配置文件 `/www/server/panel/data/session_timeout.pl` 缺失,按宝塔源码 `public.get_session_timeout()` 默认应为 `86400` 秒。
4. 宝塔 `task.py` 已经是按 `public.get_session_timeout()` 清理 session,不再是旧的硬编码 3600 秒清理。
5. 目标宝塔未开启面板 SSL`/www/server/panel/data/ssl.pl` 缺失。
6. 未开启 SSL 时,宝塔源码 `public.check_client_hash()` 会启用客户端 HASH 校验;一旦客户端 HASH 变化,宝塔会清空 session 并要求重新登录。这个更符合“同时打开 10 个宝塔,过一段时间刷新陆续掉线”的现象。
## 2. 已验证证据
### 2.1 最新 temp_login TTL
实际表位置:
```text
/www/server/panel/data/db/default.db
```
最新记录示例:
```text
id=108
state=0
add_local=2026-07-07 13:56:57
expire_local=2026-07-08 13:56:56
ttl_seconds=86399
seconds_left≈86377
```
说明 Nexus 生成的临时登录有效期约等于 24 小时。
### 2.2 宝塔登录超时配置
目标机:
```text
/www/server/panel/data/session_timeout.pl = missing
```
宝塔源码逻辑:文件缺失时默认:
```text
86400 seconds = 24 hours
```
### 2.3 宝塔 task.py session 清理
目标机 `/www/server/panel/task.py` 已存在:
```python
session_cleanup_timeout = 86400
try:
session_cleanup_timeout = int(public.get_session_timeout() or 86400)
except Exception:
pass
if f_time > session_cleanup_timeout:
os.remove(filename)
```
所以当前不是旧版每 1 小时清理 session 的问题。
### 2.4 宝塔 SSL/client_hash 风险
目标机:
```text
missing ssl.pl
```
宝塔源码中:
```python
def is_ssl():
return os.path.exists(get_panel_path() + '/data/ssl.pl')
def check_client_hash():
if is_ssl(): return True
...
if session[skey] != client_hash:
WriteLog('用户登录', '客户端HASH验证失败,已强制退出登录...建议开启面板SSL或关闭IP-HASH验证!')
return False
```
推断:如果浏览器/代理/出口 IP/UA/客户端特征变化,或者多个宝塔窗口处于不同访问链路,刷新时可能触发 client_hash 校验失败,被宝塔强制退出。
## 3. 本轮已修 Nexus 问题
发现当前 `POST /api/btpanel/servers/395/login-url` 曾返回 502
```text
ModuleNotFoundError: No module named 'public'
```
根因:Nexus SSH fallback 用系统 Python 或未完整设置宝塔源码路径执行临时登录脚本。
已修改文件:
```text
/opt/nexus-dev-current/server/infrastructure/btpanel/ssh_login.py
```
提交:
```text
fa44439 fix btpanel ssh temp login python env
```
改动要点:
1. Python 优先级调整为:
- `/www/server/panel/pyenv/bin/python3`
- `/www/server/panel/pyenv/bin/python`
- `python3`
2. 临时登录脚本执行前:
- `os.chdir('/www/server/panel')`
- 加入 `/www/server/panel`
- 加入 `/www/server/panel/class`
3. 已热更新到运行中容器并重启。
4. 验证:
- `tests/test_btpanel_login_url.py`: `6 passed`
- 一键登录接口再次返回 `200 OK`
注意:当前容器是单文件热更新;源码已提交。如果后续重新创建容器但不 rebuild 镜像,热更新会丢失。建议下一次部署时执行正式 build。
## 4. 为什么仍可能“2 小时后刷新掉线”
目前不是以下原因:
- 不是 Nexus temp token 只有 2 小时。
- 不是 `public.get_session_timeout()` 配置成 2 小时。
- 不是 `task.py` 仍按 3600 秒删除 session。
更可能是:
1. 宝塔未开启面板 SSL`check_client_hash()` 生效。
2. 多个宝塔窗口、代理链路、出口 IP、浏览器 UA 或客户端指纹发生变化。
3. 宝塔刷新请求时发现当前 session 内保存的 client_hash 与本次请求不同,于是清 session。
4. 宝塔日志里这类错误通常是“客户端 HASH 验证失败,已强制退出登录”。
## 5. 下一步修复方案(待选择)
### 方案 A:开启宝塔面板 SSL(推荐优先验证)
- 原理:宝塔源码 `if is_ssl(): return True`,开启 SSL 后跳过 client_hash 校验。
- 好处:遵循宝塔自身逻辑,不改源码。
- 风险:需要处理证书、访问协议、端口和浏览器信任问题。
- 影响:可能需要 reload/restart 宝塔。
### 方案 B:Nexus 增加宝塔登录诊断提示
- 内容:一键登录返回/审计中记录 `ttl_seconds``panel_ssl``client_hash_risk` 等非敏感信息。
- 好处:以后看到某台机器掉线,可直接确认风险点。
- 风险:要严格避免返回 token/cookie/API key。
- 影响:小改动。
### 方案 C:只对临时登录 session 放宽 client_hash
- 内容:patch 宝塔源码,若 `session['tmp_login'] == True` 则跳过或降低 client_hash 校验。
- 好处:针对 Nexus 一键登录最直接。
- 风险:改宝塔源码,升级可能覆盖;安全性低于开启 SSL。
- 影响:需要 reload/restart 宝塔。
### 方案 D:关闭宝塔 IP-HASH/client_hash 校验
- 好处:最直接。
- 风险:全局安全性下降,不建议默认。
- 影响:所有宝塔登录会话受影响。
### 方案 E:Nexus 自动定期生成新临时登录 URL
- 内容:掉线时重新点一键登录;或前端提供刷新登录入口。
- 好处:不改宝塔。
- 风险:不能解决当前会话刷新被踢,只是降低恢复成本。
- 影响:小到中。
### 方案 F:前端/代理层保活
- 内容:定时请求宝塔保持 session 活跃。
- 好处:对 session_timeout 问题有帮助。
- 风险:对 client_hash 失败基本无效;跨域限制多。
- 影响:不推荐作为主方案。
### 方案 G:Nexus 代理式宝塔访问
- 内容:Nexus 托管/代理宝塔访问链路,减少客户端 HASH 变化。
- 好处:长期可控。
- 风险:大改;安全边界复杂。
- 影响:开发量最大。
## 6. 当前推荐
推荐顺序:
1. 先选 **方案 A:开启宝塔面板 SSL**,验证多开 10 个宝塔 2 小时后刷新是否仍掉线。
2. 同时做 **方案 BNexus 诊断提示**,以后每台宝塔能直接看 TTL/SSL/client_hash 风险。
3. 如果 A 不方便或仍失败,再考虑 **方案 C:仅对临时登录放宽 client_hash**
## 7. 待办
- [ ] 用户确认选 A/B/C/D/E/F/G 哪个方向。
- [ ] 若选 A:在测试机上开启面板 SSL 并记录回滚方式。
- [ ] 若选 B:实现 Nexus 只读诊断,不返回敏感 token/cookie。
- [ ] 若选 C/D:先备份宝塔源码,再 patch,并保留一键回滚脚本。
- [ ] 正式重建 Nexus Docker 镜像,让 `fa44439` 固化进镜像。
## 8. 补充:get_client_hash 精确逻辑
后续只读查看 `/www/server/panel/class/public.py` 后确认:
```python
def get_client_hash():
is_tmp_login = session.get('tmp_login')
is_session_safe_mode = session.get('session_safe_mode')
if is_tmp_login or is_session_safe_mode:
client_hash = md5(request.remote_addr)
else:
...
```
因此对 Nexus 一键登录这种 `tmp_login=True` 的会话,宝塔主要绑定的是:
```text
request.remote_addr
```
也就是宝塔看到的客户端来源 IP。
这进一步解释了“同时打开多个宝塔,约 2 小时后刷新陆续掉线”:如果浏览器代理、网络出口、反代、CDN、VPN 或宝塔前面的转发链路导致 `request.remote_addr` 变化,宝塔刷新时会认为客户端 HASH 不一致,然后清空 session。
所以最优先验证项变为:
1. 开启宝塔面板 SSL,让 `check_client_hash()` 直接返回 True。
2. 或保证访问宝塔时来源 IP 固定。
3. 或只对 Nexus 临时登录会话放宽 `tmp_login` 的 IP 绑定。
---
## 2026-07-07 诊断字段落地与验证
### 本次代码改动
已在 Nexus 后端增加宝塔一键登录的非敏感诊断字段 `diagnostics`
- `temp_login_ttl_seconds`:Nexus 申请宝塔临时登录链接的 TTL,当前为 `86400` 秒。
- `session_cleanup_ttl_ok`:宝塔 `task.py` 会话清理阈值巡检/修复是否成功。
- `session_cleanup_ttl_patched`:本次/最近一次 SSH bootstrap 是否修补过会话清理阈值。
- `session_cleanup_ttl_checked_at`:最近一次巡检时间。
- `session_cleanup_ttl_last_error`:最近一次巡检错误码,非敏感。
- `panel_ssl`:宝塔面板是否启用面板 SSL(检测 `/www/server/panel/data/ssl.pl`),与 Nexus 的 `verify_ssl` 不是一个概念。
- `client_hash_risk`:当 `panel_ssl=false` 时为 `true`,表示宝塔临时登录 session 仍有来源 IP 变化后被 `check_client_hash()` 清掉的风险。
### 改动文件
远端测试部署机 `/opt/nexus-dev-current`
- `server/application/services/btpanel_service.py`
- `server/infrastructure/btpanel/bootstrap_state.py`
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `tests/test_btpanel_login_url.py`
### 安全边界
- 没有返回或记录 `api_key`、cookie、`tmp_token`、宝塔密码、SSH 密钥等敏感值。
- `diagnostics` 只包含 TTL、布尔状态、时间、短错误码。
- 审计日志里新增的也是同一份非敏感 `diagnostics`
- 没有直接修改宝塔 SSL 开关或宝塔源码;只是把已知风险显式化。
### 测试结果
在 Nexus 测试部署机执行:
```text
python -m py_compile server/infrastructure/btpanel/ssh_bootstrap.py \
server/infrastructure/btpanel/bootstrap_state.py \
server/application/services/btpanel_service.py \
tests/test_btpanel_login_url.py \
tests/test_btpanel_ssh_bootstrap.py
pytest tests/test_btpanel_login_url.py tests/test_btpanel_login_url_route.py tests/test_btpanel_ssh_bootstrap.py -q
# 24 passed
pytest -q
# 748 passed, 1 skipped
```
### 部署验证
已热更新 Nexus 测试容器:
```text
docker cp server/application/services/btpanel_service.py nexus-nexus-1:/app/server/application/services/btpanel_service.py
docker cp server/infrastructure/btpanel/bootstrap_state.py nexus-nexus-1:/app/server/infrastructure/btpanel/bootstrap_state.py
docker cp server/infrastructure/btpanel/ssh_bootstrap.py nexus-nexus-1:/app/server/infrastructure/btpanel/ssh_bootstrap.py
docker restart nexus-nexus-1
curl http://127.0.0.1:18600/health
# ok
```
注意:当前是测试机容器热更新,正式固化仍建议 rebuild 镜像并按发布流程部署。
### 当前结论
代码层面已能区分:
1. Nexus 一键登录 token TTL24h)。
2. 宝塔 session 清理阈值是否已修复到 24h。
3. 宝塔面板 SSL 是否开启。
4. 未开启面板 SSL 时,`tmp_login` 绑定来源 IP 导致“刷新后掉线”的风险。
下一步如果要彻底解决“多开 10 个宝塔,过 2 小时刷新陆续掉线”,优先建议在用户确认方案后处理宝塔侧:启用面板 SSL,或统一浏览器访问出口/反代来源 IP;不建议默认改宝塔 `check_client_hash()`
### 宝塔官方文档补充
宝塔官方文档「面板基础设置」中说明:面板“超时时间”默认设置为 **24 小时**,可选 1小时、6小时、12小时、24小时、3天、7天;超过该时间未操作面板会自动退出登录。参考:https://docs.bt.cn/10.0/user-guide/config/panel-settings
这与目标机源码 `public.get_session_timeout()` 在缺少 `session_timeout.pl` 时默认 86400 秒的结果一致,因此目前“约 2 小时刷新掉线”的主因仍更像是未开启面板 SSL 时的 `client_hash` 来源 IP 绑定,而不是正常超时时间到了。
## 2026-07-07 正式环境 `api.synaglobal.vip` 最小热更新记录
### 部署方式
正式主机 `/opt/nexus` 当前工作树存在大量本地修改和未跟踪前端构建文件,因此没有执行 `git pull``git reset` 或整仓覆盖。
本次采用最小风险热更新:只把测试机已验证通过的 3 个后端 Python 文件复制进正式容器 `nexus-prod-nexus-1`
- `/app/server/application/services/btpanel_service.py`
- `/app/server/infrastructure/btpanel/bootstrap_state.py`
- `/app/server/infrastructure/btpanel/ssh_bootstrap.py`
正式容器旧文件已先备份到:
```text
/tmp/nexus-bt-diag-backup-20260707-153452
```
### 部署校验
容器内 `py_compile` 通过,容器已重启,健康检查通过:
```text
container: nexus-prod-nexus-1
health: ok
status: Up About a minute (healthy)
```
部署后 3 个文件在正式容器内的 SHA256:
```text
eacf143745b14727fa66de99551e88e26c2f7ece3b4b0f82a071484da8b2dbd8 btpanel_service.py
3f84246b6b0635c3a8f74848fe606b23cac407e9a775feb729d9c0f9185ffb4a bootstrap_state.py
c4a2a1010392249dec8de68fc89def5935440df18c5c7c24a79be87763708806 ssh_bootstrap.py
```
### 正式接口验证
通过 `https://api.synaglobal.vip` 调用 `POST /api/btpanel/servers/395/login-url` 验证成功:
```text
auth_status: HTTP/1.1 200 OK
login_url_status: HTTP/1.1 200 OK
method: api
bootstrapped: false
url_sanitized: https://42.193.182.190:31701/login?tmp_token=***
```
正式接口返回的非敏感诊断字段:
```json
{
"client_hash_risk": false,
"panel_ssl": true,
"session_cleanup_ttl_checked_at": "2026-07-07T07:35:19Z",
"session_cleanup_ttl_last_error": null,
"session_cleanup_ttl_ok": true,
"session_cleanup_ttl_patched": false,
"temp_login_ttl_seconds": 86400
}
```
说明:
- `temp_login_ttl_seconds=86400`:Nexus 一键登录临时链接仍是 24 小时。
- `panel_ssl=true``client_hash_risk=false`:当前正式记录里这台宝塔面板已检测到面板 SSL 开启,因此“未开启 SSL 导致 tmp_login 绑定来源 IP”的风险在这次返回里不成立。
- `session_cleanup_ttl_ok=true`:宝塔会话清理阈值检查成功。
- `session_cleanup_ttl_patched=false`:表示本次检查没有新改动 `task.py`;不是失败。是否已经被旧补丁覆盖,要结合 `session_cleanup_ttl_ok` 和目标机源码再判断。
### 风险与后续
本次只是正式容器热更新,不是镜像固化发布。后续如果 1Panel/Docker 重新构建或重新拉镜像,这 3 个文件可能被镜像内容覆盖;建议下一步按正式发布流程把提交 `b5aaf5a feat: add btpanel login diagnostics` 合入正式镜像构建链路。
当前现网诊断结果显示 42.193.182.190 这台宝塔的 `panel_ssl=true``client_hash_risk=false`,如果用户仍复现“多开 10 个宝塔,约 2 小时后刷新陆续掉线”,下一步应优先抓以下现场:
1. 掉线前后重新调用 `login-url`,比较 `diagnostics` 是否变化。
2. 在宝塔机上检查 session 文件创建/删除时间和 `request.remote_addr` 来源变化。
3. 检查浏览器/代理/反代是否对不同标签页或不同时间切换出口。
4. 检查宝塔自身是否还有其它 session 清理逻辑或安全插件清理逻辑。
5. 检查 Nexus 返回的 `method` 是否一直为 `api`,是否在某些机器上触发 SSH bootstrap 或 fallback。
@@ -0,0 +1,987 @@
# Nexus 宝塔一键登录长期掉线修复方案与代码改进
生成时间:2026-07-07Asia/Shanghai
项目路径:`C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus`
生产 API`https://api.synaglobal.vip`
排查机器:`42.193.182.190`
Nexus server id`395`
> 本文档不包含任何密码、token、私钥、API key。所有敏感凭据均已省略。
---
## 1. 问题背景
用户反馈:
- Nexus 一键登录宝塔本身能成功。
- 同时打开多个宝塔面板后,约 1~2 小时刷新会陆续掉线。
- 目标机器示例:`42.193.182.190`
一开始怀疑点:
1. Nexus 生成的宝塔临时登录 token TTL 太短。
2. Nexus 一键登录 URL 缓存导致返回已消费的一次性 token。
3. 多 worker / 多请求并发生成宝塔临时登录,互相覆盖或失效。
4. 宝塔自身 session 清理策略导致登录态被清掉。
最终确认:**根因是第 4 点,宝塔自身 `BT-Task` 定期清理 session 文件时硬编码 3600 秒。**
---
## 2. 排查结论
### 2.1 Nexus 生产生成的一键登录 token 已经是 24 小时
通过生产 Nexus 调用:
```http
POST /api/btpanel/servers/395/login-url
```
返回:
```text
method=api
url=https://42.193.182.190:31701/login?tmp_token=***
```
随后在目标机宝塔 DB 查到最新 `temp_login`
```text
state=0
ttl_seconds≈86399
remain_seconds≈86398
```
因此 Nexus 当前生产生成的宝塔临时登录不是 2/3 小时,而是约 **24 小时**
### 2.2 目标机器宝塔配置也是 24 小时
目标机器:`42.193.182.190`
```text
/www/server/panel/data/session_timeout.pl = 86400
宝塔版本:11.0.0
宝塔端口:31701
```
### 2.3 真正根因:宝塔 task.py 硬编码清理 1 小时 session
宝塔后台任务文件:
```text
/www/server/panel/task.py
```
原始逻辑片段:
```python
if f_time > 3600:
os.remove(filename)
continue
```
宝塔每 10 分钟跑一次 session 清理,超过 3600 秒的 session 文件会被删除。
临时登录刷新时又依赖 session 文件存在。相关逻辑在:
```text
/www/server/panel/class/common.py
```
核心逻辑:
```python
if 'tmp_login_expire' in session:
s_file = 'data/session/{}'.format(session['tmp_login_id'])
if session['tmp_login_expire'] < time.time():
session.clear()
if os.path.exists(s_file):
os.remove(s_file)
return ...
if not os.path.exists(s_file):
session.clear()
return ...
```
也就是说:
```text
Nexus 一键登录成功
→ 宝塔生成 tmp_login session 文件
→ BT-Task 每 10 分钟清理 session
→ task.py 硬编码超过 3600 秒删除
→ 用户 12 小时后刷新
→ tmp_login session 文件不存在
→ 宝塔清空 session
→ 用户被踢出
```
---
## 3. 总体修复方案
### 3.1 Nexus 侧改进
Nexus 后端做四类改进:
1. **不缓存成功的一键登录 URL**
- 宝塔 `tmp_token` 是一次性链接。
- 如果缓存成功 URL,用户再次拿到的可能是已消费 token。
2. **一键登录生成加 Redis 分布式锁**
- 同一台宝塔服务器同时只允许一个 Nexus worker 生成登录 URL。
- 避免 10 个页面同时一键登录时互相覆盖临时登录 token。
3. **宝塔临时登录 TTL 固定为 86400 秒**
- `set_temp_login` 传入 24 小时过期时间。
4. **Nexus bootstrap/repair 自动修复宝塔 session 清理硬编码**
- 检查 `/www/server/panel/task.py`
- 把 `if f_time > 3600:` 改为 `if f_time > session_cleanup_timeout:`
- `session_cleanup_timeout` 从宝塔 `public.get_session_timeout()` 读取。
- 修改前备份。
- 修改前做 Python 语法校验。
- 保留原文件权限。
### 3.2 目标机器热修复
已对 `42.193.182.190` 执行热修复:
- 备份:`/www/server/panel/task.py.nexus-session-ttl.bak`
- 修改:`/www/server/panel/task.py`
- reload`/etc/init.d/bt reload`
- 状态:`Bt-Panel``Bt-Task` 均 running
---
## 4. 调用链图:Nexus 一键登录 API / 服务 / 数据库 / 宝塔调用链
```mermaid
flowchart TD
A["前端点击:宝塔一键登录"] --> B["POST /api/btpanel/servers/{server_id}/login-url"]
B --> C["server/api/btpanel.py:create_bt_login_url"]
C --> D["BtPanelService.create_login_url"]
D --> E["Redis 分布式锁 login_url_lock(server_id)"]
E --> F["_create_login_url_fresh"]
F --> G{"bt_panel credentials 是否已配置"}
G -- "否" --> H["bootstrap_server(force=True)"]
H --> I["ssh_bootstrap_panel"]
I --> J["远程修改宝塔 api.json / 白名单 / session cleanup TTL"]
J --> K["更新 Server.extra_attrs.bt_panel"]
G -- "是" --> L["read_bt_panel_credentials"]
K --> L
L --> M["BtPanelClient.post /config?action=set_temp_login"]
M --> N["宝塔 default.db.temp_login 写入临时登录记录"]
N --> O["resolve_temp_login_url / normalize_temp_login_url"]
O --> P["写 AuditLog: bt_panel_login_url"]
P --> Q["返回 login URL 给前端"]
```
---
## 5. 本地 Nexus 代码改进清单
### 5.1 新增 Redis 分布式锁
文件:
```text
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\infrastructure\btpanel\login_url_lock.py
```
完整新增代码:
```python
"""Distributed per-server lock for BT Panel one-click login URL generation.
BT Panel temporary login tokens are effectively single-use and creating a new token can
invalidate a previous unused token. A process-local ``asyncio.Lock`` is not sufficient
when Nexus runs with multiple workers/containers, so this lock is backed by Redis.
"""
from __future__ import annotations
import asyncio
import logging
import secrets
from contextlib import asynccontextmanager
from typing import AsyncIterator
from server.infrastructure.redis.client import get_redis
logger = logging.getLogger("nexus.btpanel.login_url_lock")
_LOCK_KEY_PREFIX = "btpanel:login_url:lock"
_LOCK_TTL_SECONDS = 600
_LOCK_WAIT_TIMEOUT_SECONDS = 120.0
_LOCK_POLL_SECONDS = 0.2
_RELEASE_SCRIPT = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0
"""
class LoginUrlLockTimeout(RuntimeError):
"""Raised when another worker is already generating a login URL too long."""
def _lock_key(server_id: int) -> str:
return f"{_LOCK_KEY_PREFIX}:{server_id}"
@asynccontextmanager
async def login_url_lock(server_id: int) -> AsyncIterator[None]:
"""Serialize login URL generation across all Nexus workers.
Redis is mandatory in Nexus, so lock backend errors are treated as hard failures
rather than silently falling back to an unsafe process-local lock.
"""
redis = get_redis()
key = _lock_key(server_id)
token = secrets.token_urlsafe(24)
deadline = asyncio.get_running_loop().time() + _LOCK_WAIT_TIMEOUT_SECONDS
while True:
acquired = bool(await redis.set(key, token, ex=_LOCK_TTL_SECONDS, nx=True))
if acquired:
break
if asyncio.get_running_loop().time() >= deadline:
raise LoginUrlLockTimeout("BT Panel login URL is already being generated; please retry shortly")
await asyncio.sleep(_LOCK_POLL_SECONDS)
try:
yield
finally:
try:
await redis.eval(_RELEASE_SCRIPT, 1, key, token)
except Exception: # noqa: BLE001 - lock expires automatically; log release failures for ops visibility
logger.warning("failed to release bt panel login url lock server=%s", server_id, exc_info=True)
```
设计点:
- 锁 key`btpanel:login_url:lock:{server_id}`
- 锁 TTL600 秒,避免 worker 崩溃后死锁。
- 等待超时:120 秒。
- Lua 释放锁,避免误删其它请求持有的新锁。
---
### 5.2 登录 URL 不再缓存,始终 fresh 生成
文件:
```text
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\application\services\btpanel_service.py
```
核心代码:
```python
async def create_login_url(
self,
server_id: int,
*,
admin_username: str,
ip_address: str | None,
) -> dict[str, str | bool]:
# BT Panel tmp_token is a one-time login link. Do not cache successful
# URLs: a repeated request after the user opens the link could receive
# an already-consumed token. Redis lock still prevents concurrent
# workers from creating tokens at the same time and invalidating each other.
try:
async with login_url_lock(server_id):
return await self._create_login_url_fresh(
server_id,
admin_username=admin_username,
ip_address=ip_address,
)
except Exception as exc:
await self._audit_login_url_failure(
server_id,
admin_username=admin_username,
ip_address=ip_address,
error=exc.__class__.__name__,
)
raise
```
关键变化:
- 成功 URL 不缓存。
- 所有一键登录请求进入 Redis 分布式锁。
- 异常统一写 audit,便于排查。
---
### 5.3 临时登录 token TTL 固定为 24 小时
文件:
```text
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\infrastructure\btpanel\constants.py
```
代码:
```python
"""Baota panel integration constants."""
# Temporary login token/session upper bound in seconds; passed to BT Panel set_temp_login expire_time.
BT_TEMP_LOGIN_TTL_SECONDS = 86400
# Login URL success results are intentionally not cached: BT Panel tmp_token is one-time.
# Concurrent generation is guarded by the Redis distributed lock in login_url_lock.py.
```
调用点:
```python
expire_time = int(time.time()) + BT_TEMP_LOGIN_TTL_SECONDS
client = BtPanelClient(creds, server.id)
data = await client.post(
"/config?action=set_temp_login",
{"expire_time": expire_time},
)
```
效果:
- Nexus 调宝塔 `set_temp_login` 时传入 24 小时绝对过期时间。
- 生产实测 `temp_login` 记录 TTL 约 86399 秒。
---
### 5.4 API 层把锁超时映射为 409
文件:
```text
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\api\btpanel.py
```
代码:
```python
def _http_error(exc: Exception) -> HTTPException:
if isinstance(exc, ValueError):
return HTTPException(status_code=400, detail=str(exc))
if isinstance(exc, BtPanelApiError):
return HTTPException(status_code=502, detail=str(exc))
if isinstance(exc, LoginUrlLockTimeout):
return HTTPException(status_code=409, detail=str(exc))
if isinstance(exc, RuntimeError):
return HTTPException(status_code=502, detail=str(exc))
logger.exception("btpanel error")
return HTTPException(status_code=500, detail="宝塔操作失败")
```
设计点:
- 同一服务器一键登录并发过多时,返回 `409 Conflict`
- 前端可以提示“正在生成登录链接,请稍后重试”。
- 不返回 500,避免误判成系统异常。
---
### 5.5 Nexus bootstrap 自动修复宝塔 session 清理 TTL
文件:
```text
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\infrastructure\btpanel\ssh_bootstrap.py
```
新增函数:
```python
def ensure_session_cleanup_ttl():
# Make Baota session cleanup honor session_timeout.pl instead of a hard-coded 1h.
task_path = os.path.join(PANEL_ROOT, "task.py")
if not os.path.isfile(task_path):
return False
with open(task_path, "r", encoding="utf-8") as f:
source = f.read()
marker = "session_cleanup_timeout = int(public.get_session_timeout() or 86400)"
patched_condition = " if f_time > session_cleanup_timeout:\n"
if marker in source:
if patched_condition not in source:
raise RuntimeError("task.py session cleanup patch incomplete")
return False
anchor = (
" s_time = time.time()\n"
" f_list = os.listdir(sess_path)\n"
" f_num = len(f_list)\n"
)
replacement = (
" s_time = time.time()\n"
" session_cleanup_timeout = 86400\n"
" try:\n"
" session_cleanup_timeout = int(public.get_session_timeout() or 86400)\n"
" except Exception:\n"
" pass\n"
" f_list = os.listdir(sess_path)\n"
" f_num = len(f_list)\n"
)
hard_coded = " if f_time > 3600:\n"
anchor_index = source.find(anchor)
if anchor_index < 0:
raise RuntimeError("task.py session cleanup anchor not found")
patched = source[:anchor_index] + replacement + source[anchor_index + len(anchor):]
condition_index = patched.find(hard_coded, anchor_index + len(replacement))
if condition_index < 0:
raise RuntimeError("task.py session cleanup threshold not found")
patched = patched[:condition_index] + patched_condition + patched[condition_index + len(hard_coded):]
compile(patched, task_path, "exec")
original_mode = os.stat(task_path).st_mode & 0o7777
backup = task_path + ".nexus-session-ttl.bak"
if not os.path.exists(backup):
with open(backup, "w", encoding="utf-8") as f:
f.write(source)
os.chmod(backup, 0o600)
tmp = task_path + ".nexus.tmp"
with open(tmp, "w", encoding="utf-8") as f:
f.write(patched)
os.chmod(tmp, original_mode)
os.replace(tmp, task_path)
return True
```
接入 bootstrap 主流程:
```python
actions = []
warnings = []
try:
if ensure_session_cleanup_ttl():
actions.append("patched_session_cleanup_ttl")
except Exception as exc:
warnings.append(f"session_cleanup_ttl_patch_failed:{exc.__class__.__name__}")
```
返回结果新增 warnings
```python
emit({
"ok": True,
"base_url": base,
"api_key": token,
"actions": actions,
"warnings": warnings,
"reloaded": bool(actions),
"verify_ssl": False,
})
```
设计点:
- **精确匹配**:只匹配 `sess_expire``s_time/f_list/f_num` 附近的固定片段。
- **幂等**:已经包含 `session_cleanup_timeout` 时不重复 patch。
- **半补丁检测**:有 marker 但没有新判断时报警。
- **备份**:首次修改前备份到 `task.py.nexus-session-ttl.bak`
- **语法校验**:写入前 `compile(patched, task_path, "exec")`
- **权限保留**:新文件保留原 `task.py` mode。
- **不阻断 bootstrap**patch 失败只进 `warnings`,不影响 API token/bootstrap 主流程。
---
## 6. 测试改进
文件:
```text
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\tests\test_btpanel_ssh_bootstrap.py
```
新增测试:
```python
def test_remote_bootstrap_patches_session_cleanup_ttl():
from server.infrastructure.btpanel import ssh_bootstrap as mod
script = mod._REMOTE_SCRIPT
assert "patched_session_cleanup_ttl" in script
assert "session_cleanup_timeout = int(public.get_session_timeout() or 86400)" in script
assert "if f_time > session_cleanup_timeout" in script
assert "hard_coded = \" if f_time > 3600:\\n\"" in script
assert "compile(patched, task_path, \"exec\")" in script
assert "original_mode = os.stat(task_path).st_mode & 0o7777" in script
assert "os.chmod(tmp, original_mode)" in script
assert ".nexus-session-ttl.bak" in script
```
已做静态验证:
```text
compile ok server\infrastructure\btpanel\ssh_bootstrap.py
remote script compile ok
compile ok tests\test_btpanel_ssh_bootstrap.py
```
完整 pytest 暂未跑完,原因见第 11 节。
---
## 7. 生产热修复:42.193.182.190
### 7.1 已执行动作
通过生产 Nexus API 走 SOCKS5
```text
SOCKS5: 127.0.0.1:10808
Nexus API: https://api.synaglobal.vip
server_id: 395
```
对目标机执行:
1. 检查 `/www/server/panel/task.py` 当前逻辑。
2. 备份为 `/www/server/panel/task.py.nexus-session-ttl.bak`
3. 修改 session 清理阈值。
4. reload 宝塔。
5. 回查修改结果和宝塔状态。
### 7.2 生产执行结果
执行结果:
```text
exec_status HTTP/1.1 200 OK execution_id 5917
status completed exit 0
```
修复前:
```text
998: if f_time > 3600:
```
修复后:
```text
992: session_cleanup_timeout = 86400
994: session_cleanup_timeout = int(public.get_session_timeout() or 86400)
1003: if f_time > session_cleanup_timeout:
```
备份:
```text
backup 600 root:root 61810 2026-07-07 05:08:05 +0800 /www/server/panel/task.py.nexus-session-ttl.bak
```
宝塔状态:
```text
Bt-Panel already running
Bt-Task already running
```
---
## 8. 生产远程修复脚本模板
> 注意:以下脚本不包含任何 Nexus 登录凭据。实际执行应通过 Nexus 脚本执行功能或 SSH,在目标机 root/sudo 环境运行。
```bash
set -e
P=/www/server/panel/pyenv/bin/python3
[ -x "$P" ] || P=/www/server/panel/pyenv/bin/python
[ -x "$P" ] || P=python3
RESULT_FILE=/tmp/nexus_bt_session_ttl_patch.result
rm -f "$RESULT_FILE"
cat /www/server/panel/data/session_timeout.pl 2>/dev/null | sed 's/^/session_timeout=/' || true
sudo grep -n 'session_cleanup_timeout\|if f_time > 3600\|if f_time > session_cleanup_timeout' /www/server/panel/task.py || true
sudo "$P" - <<'PY'
import os
import py_compile
import stat
PANEL_ROOT = "/www/server/panel"
task_path = os.path.join(PANEL_ROOT, "task.py")
result_file = "/tmp/nexus_bt_session_ttl_patch.result"
with open(task_path, "r", encoding="utf-8") as f:
source = f.read()
marker = "session_cleanup_timeout = int(public.get_session_timeout() or 86400)"
patched_condition = " if f_time > session_cleanup_timeout:\n"
if marker in source:
if patched_condition not in source:
raise SystemExit("patch_incomplete")
result = "already_patched"
else:
anchor = (
" s_time = time.time()\n"
" f_list = os.listdir(sess_path)\n"
" f_num = len(f_list)\n"
)
replacement = (
" s_time = time.time()\n"
" session_cleanup_timeout = 86400\n"
" try:\n"
" session_cleanup_timeout = int(public.get_session_timeout() or 86400)\n"
" except Exception:\n"
" pass\n"
" f_list = os.listdir(sess_path)\n"
" f_num = len(f_list)\n"
)
hard_coded = " if f_time > 3600:\n"
anchor_index = source.find(anchor)
if anchor_index < 0:
raise SystemExit("anchor_not_found")
patched = source[:anchor_index] + replacement + source[anchor_index + len(anchor):]
condition_index = patched.find(hard_coded, anchor_index + len(replacement))
if condition_index < 0:
raise SystemExit("threshold_not_found")
patched = patched[:condition_index] + patched_condition + patched[condition_index + len(hard_coded):]
backup = task_path + ".nexus-session-ttl.bak"
if not os.path.exists(backup):
with open(backup, "w", encoding="utf-8") as f:
f.write(source)
os.chmod(backup, 0o600)
st = os.stat(task_path)
tmp = task_path + ".nexus.tmp"
with open(tmp, "w", encoding="utf-8") as f:
f.write(patched)
os.chmod(tmp, stat.S_IMODE(st.st_mode))
py_compile.compile(tmp, doraise=True)
os.replace(tmp, task_path)
result = "patched"
with open(result_file, "w", encoding="utf-8") as f:
f.write(result)
print(result)
PY
RESULT=$(cat "$RESULT_FILE")
echo "patch_result=$RESULT"
if [ "$RESULT" = "patched" ]; then
sudo /etc/init.d/bt reload || true
fi
sudo grep -n 'session_cleanup_timeout\|if f_time > 3600\|if f_time > session_cleanup_timeout' /www/server/panel/task.py || true
sudo stat -c 'backup %a %U:%G %s %y %n' /www/server/panel/task.py.nexus-session-ttl.bak 2>/dev/null || true
sudo /etc/init.d/bt status 2>/dev/null || true
```
---
## 9. 部署方案
### 9.1 本地代码部署到 Nexus 生产
建议流程:
1. 备份当前生产 Nexus 代码/镜像。
2. 合入以下文件变更:
```text
server/infrastructure/btpanel/login_url_lock.py
server/infrastructure/btpanel/constants.py
server/application/services/btpanel_service.py
server/api/btpanel.py
server/infrastructure/btpanel/ssh_bootstrap.py
tests/test_btpanel_ssh_bootstrap.py
```
3. 构建后端镜像。
4. 发布到生产。
5. 重启后端服务。
6. 回归测试:
```http
POST /api/btpanel/servers/{server_id}/login-url
```
7. 检查 audit
```text
action=bt_panel_login_url
method=api 或 ssh
lock=redis
```
8. 对一台测试机器触发 bootstrap/repair,确认返回:
```json
{
"actions": ["patched_session_cleanup_ttl"],
"warnings": [],
"reloaded": true
}
```
或者已修复时:
```json
{
"actions": [],
"warnings": [],
"reloaded": false
}
```
### 9.2 对已有机器批量修复
建议分批执行,不要一次修全部:
1. 先选 1 台验证。
2. 再选 510 台。
3. 最后批量。
每台机器执行前检查:
```bash
cat /www/server/panel/data/session_timeout.pl
sudo grep -n 'if f_time > 3600\|session_cleanup_timeout' /www/server/panel/task.py
```
执行后检查:
```bash
sudo grep -n 'session_cleanup_timeout\|if f_time > session_cleanup_timeout' /www/server/panel/task.py
sudo stat /www/server/panel/task.py.nexus-session-ttl.bak
sudo /etc/init.d/bt status
```
---
## 10. 回滚方案
### 10.1 单机宝塔回滚
如果某台宝塔修复后异常,可以回滚:
```bash
sudo cp -a /www/server/panel/task.py /www/server/panel/task.py.nexus-session-ttl.rollback-copy
sudo cp -a /www/server/panel/task.py.nexus-session-ttl.bak /www/server/panel/task.py
sudo /etc/init.d/bt reload
sudo /etc/init.d/bt status
```
回滚后会恢复宝塔原始 3600 秒 session 清理行为。
### 10.2 Nexus 代码回滚
回滚以下逻辑即可:
- `ssh_bootstrap.py``ensure_session_cleanup_ttl()` 及调用。
- `login_url_lock.py``create_login_url()` 内 Redis 锁使用。
- `constants.py` 中 86400 TTL 如需恢复原值。
- `btpanel.py``LoginUrlLockTimeout` 的 409 映射。
不建议回滚“不缓存 login URL”的逻辑,因为宝塔 `tmp_token` 是一次性链接,缓存成功 URL 本身有风险。
---
## 11. 依赖安装与测试现状
### 11.1 已创建虚拟环境
路径:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv
```
Python / pip
```text
pip 26.0.1
python 3.14
```
### 11.2 安装依赖尝试
用户要求安装:
```text
pytest/sqlalchemy
```
项目依赖文件:
```text
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\requirements.txt
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\requirements-dev.txt
```
尝试 SOCKS5 安装:
```powershell
python -m pip install --proxy socks5://127.0.0.1:10808 -r requirements.txt -r requirements-dev.txt
```
失败原因:
```text
ERROR: Could not install packages due to an OSError: Missing dependencies for SOCKS support.
```
含义:当前 venv 的 pip/requests 缺 `PySocks`,所以不能直接识别 `socks5://`
尝试直连安装:
```powershell
python -m pip install -r requirements.txt -r requirements-dev.txt
```
失败原因:
```text
SSLError(SSLEOFError: UNEXPECTED_EOF_WHILE_READING)
No matching distribution found for fastapi==0.115.6
```
含义:直连 PyPI SSL 被中断或网络不可用。
### 11.3 推荐安装方案
方案 A:如果本机有 HTTP 代理端口,优先用 HTTP 代理安装:
```powershell
$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv"
$proj = "C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus"
& "$venv\Scripts\python.exe" -m pip install --proxy http://127.0.0.1:端口 -r "$proj\requirements.txt" -r "$proj\requirements-dev.txt"
```
方案 B:先离线/手动安装 PySocks,再走 SOCKS5
```powershell
$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv"
& "$venv\Scripts\python.exe" -m pip install PySocks
& "$venv\Scripts\python.exe" -m pip install --proxy socks5://127.0.0.1:10808 -r requirements.txt -r requirements-dev.txt
```
但 B 的第一步仍需要能联网,所以如果直连不可用,仍建议使用 HTTP 代理或镜像源。
方案 C:使用国内镜像源:
```powershell
$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv"
$proj = "C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus"
& "$venv\Scripts\python.exe" -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r "$proj\requirements.txt" -r "$proj\requirements-dev.txt"
```
### 11.4 依赖安装成功后运行测试
```powershell
$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv"
$proj = "C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus"
cd $proj
$env:PYTHONDONTWRITEBYTECODE = "1"
& "$venv\Scripts\python.exe" -m pytest tests/test_btpanel_ssh_bootstrap.py tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_login_url.py -q
```
---
## 12. 代码审查结果
### 12.1 已解决风险
| 风险 | 原状态 | 改进后 |
|---|---|---|
| 宝塔 tmp_token 一次性 URL 被缓存 | 可能返回已消费 token | 不缓存成功 login URL |
| 多 worker 并发生成 token | 可能互相覆盖/失效 | Redis per-server lock |
| token TTL 太短 | 已确认不是短 TTL,但需固定 | 统一 86400 秒 |
| 宝塔 session 文件 3600 秒硬清 | 12 小时刷新掉线 | 跟随 session_timeout.pl |
| 宝塔 task.py 误改风险 | 直接替换可能误伤 | 精确 anchor + 语法校验 |
| 远程修复不可回滚 | 无备份风险 | `.nexus-session-ttl.bak` 备份 |
| 文件权限变化 | 可能破坏宝塔运行 | 保留原 task.py mode |
| bootstrap patch 失败影响主流程 | 可能导致 API 配置失败 | warning,不阻断主 bootstrap |
### 12.2 剩余注意事项
1. **宝塔升级可能覆盖 `task.py`**
- 升级后需要重新触发 Nexus bootstrap/repair 或批量修复。
2. **当前不是永久登录**
- 当前按 `session_timeout.pl=86400`,约 24 小时。
- 如果想更久,需要调大宝塔 `session_timeout.pl`,并接受安全风险。
3. **reload 宝塔可能让当前面板短暂重连**
- 单台修复时影响较小。
- 批量修复建议低峰执行。
4. **Redis 必须可用**
- `login_url_lock` 不做不安全 fallback。
- Redis 故障时一键登录会失败,避免并发隐患静默放大。
---
## 13. 验证清单
### 13.1 立即验证
`42.193.182.190`
```bash
sudo grep -n 'session_cleanup_timeout\|if f_time > session_cleanup_timeout' /www/server/panel/task.py
sudo stat /www/server/panel/task.py.nexus-session-ttl.bak
sudo /etc/init.d/bt status
```
期望:
```text
session_cleanup_timeout = 86400
session_cleanup_timeout = int(public.get_session_timeout() or 86400)
if f_time > session_cleanup_timeout
Bt-Panel running
Bt-Task running
```
### 13.2 业务验证
1. Nexus 一键登录 `42.193.182.190`
2. 同时打开 10 个宝塔页面。
3. 等待 2 小时。
4. 刷新页面。
5. 期望:不再因为 3600 秒 session 清理导致掉线。
### 13.3 24 小时边界验证
如果需要验证 24 小时边界:
- 24 小时内刷新应保持登录。
- 超过 `session_timeout.pl` 后刷新可能要求重新登录,这是预期行为。
---
## 14. 最终结论
本次修复后,`42.193.182.190` 的“宝塔一键登录后 1~2 小时刷新掉线”问题已针对根因修复:
```text
宝塔 task.py 不再硬编码 3600 秒清理 session,改为跟随 session_timeout.pl。
```
Nexus 后端也已补上长期改进:
```text
不缓存一次性登录 URL
+ Redis 分布式锁串行化同一服务器登录 URL 生成
+ 24 小时 temp_login TTL
+ bootstrap 自动修复宝塔 session 清理 TTL
+ audit/warnings 可观测
```
建议下一步:
1. 安装 Python 测试依赖并跑相关 pytest。
2. 把本地 Nexus 改动发布到生产。
3. 对其它宝塔机器分批执行同类修复。
4. 加一个周期性巡检:检测 `task.py` 是否被宝塔升级覆盖。
@@ -0,0 +1,91 @@
# 宝塔批量 Session 巡检共性分析(2026-07-07
数据来源:`work/bt_bulk_session_audit.jsonl` + `work/bulk_servers_list.json`。本次为离线聚合分析,未重新 SSH 扫描服务器。
## 总体
在线巡检记录:420 台。
| 分类 | 数量 |
|---|---:|
| Alibaba Cloud Linux / OpenAnolis | 200 |
| Ubuntu | 155 |
| Debian | 65 |
## hardcoded_3600 的 148 台
结论:不是 Ubuntu 为主,不全是 Ubuntu。
| OS | 数量 |
|---|---:|
| Alibaba Cloud Linux / OpenAnolis | 77 |
| Debian 12 bookworm | 43 |
| Ubuntu | 28 |
更细:
| 版本 | 数量 |
|---|---:|
| Alibaba Cloud Linux 4.0.3 | 49 |
| Debian GNU/Linux 12 bookworm | 43 |
| Ubuntu 24.04.4 LTS | 25 |
| Alibaba Cloud Linux 3.2104 U12.2 | 21 |
| Alibaba Cloud Linux 3.2104 U12.3 | 6 |
| Ubuntu 24.04.3 LTS | 3 |
| Alibaba Cloud Linux 3.2104 U12.1 | 1 |
共性:
- 148 台全部 `panel_ssl=1`
- 148 台全部 `check_client_hash=1`
- 148 台全部 `session_timeout=86400`,说明配置值存在。
- 但 `task.py` 仍匹配到硬编码清理逻辑 `f_time > 3600`,所以属于“配置是 86400,但清理代码仍按 3600 秒删 session”的类型。
- 主机名分布:阿里云风格 `iZ*` 114 台,腾讯云风格 `VM-*` 34 台。
## 未开启面板 SSL 的 112 台
结论:这批基本是 Ubuntu,但不是 100%。
| OS | 数量 |
|---|---:|
| Ubuntu | 104 |
| Alibaba Cloud Linux / OpenAnolis | 5 |
| Debian | 3 |
更细:
| 版本 | 数量 |
|---|---:|
| Ubuntu 24.04.4 LTS | 104 |
| Alibaba Cloud Linux 3.2104 U12.3 | 4 |
| Debian GNU/Linux 12 bookworm | 3 |
| Alibaba Cloud Linux 3.2104 U13 | 1 |
共性:
- 112 台全部 `panel_ssl=0`
- 111 台是 `unknown_task_pattern`1 台 `no_panel`
- 105 台 `session_timeout=missing_default_86400`
- 105 台 `check_client_hash=0`
- 主机名分布:腾讯云风格 `VM-*` 98 台,阿里云风格 `iZ*` 7 台,其它自定义主机名 7 台。
## 关键关系
`hardcoded_3600``panel_ssl=0` 当前没有重叠:
| 组合 | 数量 |
|---|---:|
| hardcoded_3600 且 panel_ssl=0 | 0 |
| hardcoded_3600 且 panel_ssl=1 | 148 |
| unknown_task_pattern 且 panel_ssl=0 | 111 |
所以这不是同一类问题:
1. `hardcoded_3600`:主要是宝塔 session 清理代码硬编码 3600 秒,且面板 SSL 已开启。
2. `panel_ssl=0`:主要是 Ubuntu 24.04.4 + 腾讯云 VM-* 这一批,面板未开 SSL,且 task.py 代码模式与前一批不同,需要抽样后分类修。
## 处理建议
1. 先处理 148 台 `hardcoded_3600`:备份 `/www/server/panel/task.py` 后把 3600 秒硬编码改成读取 `public.get_session_timeout()`,这批模式清晰。
2. 对 112 台 `panel_ssl=0` 不要套同一个 patch;先抽样 5~10 台看 `task.py``public.py/check_client_hash` 版本差异。
3. `panel_ssl=0` 是否要统一开启 SSL,是策略问题,不建议默认自动改;需要单独定方案。
@@ -0,0 +1,138 @@
# Nexus 宝塔批量 Session 巡检报告 - 2026-07-07
- 总服务器数:467
- 在线目标数:420
- 巡检结果数:420
## task.py session 清理状态统计
- `error`: 2
- `no_panel`: 1
- `patched_timeout`: 149
- `unknown_task_pattern`: 268
## 面板 SSL 状态统计
- `panel_ssl=0`: 112
- `panel_ssl=1`: 306
- `panel_ssl=None`: 2
## 需要修复 `task.py 3600` 的机器:0
| server_id | domain | task_state | session_timeout | panel_ssl |
|---:|---|---|---|---:|
## 未开启面板 SSL 的宝塔机器:112
| server_id | domain | task_state | session_timeout | panel_ssl |
|---:|---|---|---|---:|
| 508 | 119.29.98.54 | no_panel | missing_default_86400 | 0 |
| 509 | 43.139.95.214 | unknown_task_pattern | missing_default_86400 | 0 |
| 505 | 42.193.140.122 | unknown_task_pattern | missing_default_86400 | 0 |
| 506 | 111.230.27.20 | unknown_task_pattern | missing_default_86400 | 0 |
| 507 | 106.55.44.161 | unknown_task_pattern | missing_default_86400 | 0 |
| 503 | 111.230.71.100 | unknown_task_pattern | missing_default_86400 | 0 |
| 504 | 134.175.126.92 | unknown_task_pattern | missing_default_86400 | 0 |
| 510 | 159.75.154.203 | unknown_task_pattern | missing_default_86400 | 0 |
| 489 | 139.199.59.208 | unknown_task_pattern | missing_default_86400 | 0 |
| 490 | 119.91.117.54 | unknown_task_pattern | missing_default_86400 | 0 |
| 467 | 111.230.90.201 | unknown_task_pattern | missing_default_86400 | 0 |
| 468 | 119.29.80.171 | unknown_task_pattern | missing_default_86400 | 0 |
| 453 | 119.29.114.23 | unknown_task_pattern | missing_default_86400 | 0 |
| 454 | 175.178.40.17 | unknown_task_pattern | missing_default_86400 | 0 |
| 452 | 43.139.143.211 | unknown_task_pattern | missing_default_86400 | 0 |
| 449 | 1.12.47.125 | unknown_task_pattern | missing_default_86400 | 0 |
| 450 | 106.55.22.208 | unknown_task_pattern | missing_default_86400 | 0 |
| 451 | 134.175.227.87 | unknown_task_pattern | missing_default_86400 | 0 |
| 448 | 43.138.249.111 | unknown_task_pattern | missing_default_86400 | 0 |
| 447 | 122.152.235.93 | unknown_task_pattern | missing_default_86400 | 0 |
| 446 | 42.193.252.88 | unknown_task_pattern | missing_default_86400 | 0 |
| 445 | 134.175.67.244 | unknown_task_pattern | missing_default_86400 | 0 |
| 430 | 20.2.235.101 | unknown_task_pattern | missing_default_86400 | 0 |
| 443 | 42.193.214.33 | unknown_task_pattern | missing_default_86400 | 0 |
| 436 | 106.52.242.218 | unknown_task_pattern | missing_default_86400 | 0 |
| 431 | 193.112.70.74 | unknown_task_pattern | missing_default_86400 | 0 |
| 432 | 43.139.133.179 | unknown_task_pattern | missing_default_86400 | 0 |
| 435 | 139.199.72.167 | unknown_task_pattern | missing_default_86400 | 0 |
| 433 | 119.91.195.218 | unknown_task_pattern | missing_default_86400 | 0 |
| 429 | 43.138.248.173 | unknown_task_pattern | missing_default_86400 | 0 |
| 444 | 159.75.29.226 | unknown_task_pattern | missing_default_86400 | 0 |
| 428 | 123.207.4.3 | unknown_task_pattern | missing_default_86400 | 0 |
| 422 | 104.214.187.235 | unknown_task_pattern | missing_default_86400 | 0 |
| 434 | 42.194.204.225 | unknown_task_pattern | missing_default_86400 | 0 |
| 424 | 159.75.203.210 | unknown_task_pattern | missing_default_86400 | 0 |
| 426 | 42.194.226.62 | unknown_task_pattern | missing_default_86400 | 0 |
| 425 | 119.29.93.168 | unknown_task_pattern | missing_default_86400 | 0 |
| 427 | 106.52.179.21 | unknown_task_pattern | missing_default_86400 | 0 |
| 418 | 106.53.75.192 | unknown_task_pattern | missing_default_86400 | 0 |
| 417 | 119.91.139.218 | unknown_task_pattern | missing_default_86400 | 0 |
| 416 | 42.193.200.77 | unknown_task_pattern | missing_default_86400 | 0 |
| 414 | 42.193.186.43 | unknown_task_pattern | missing_default_86400 | 0 |
| 415 | 114.132.210.151 | unknown_task_pattern | missing_default_86400 | 0 |
| 406 | 20.189.126.177 | unknown_task_pattern | missing_default_86400 | 0 |
| 404 | 104.214.176.182 | unknown_task_pattern | missing_default_86400 | 0 |
| 403 | 40.81.22.70 | unknown_task_pattern | missing_default_86400 | 0 |
| 412 | 1.14.141.27 | unknown_task_pattern | missing_default_86400 | 0 |
| 411 | 119.29.18.166 | unknown_task_pattern | missing_default_86400 | 0 |
| 410 | 43.138.205.6 | unknown_task_pattern | missing_default_86400 | 0 |
| 413 | 159.75.108.190 | unknown_task_pattern | missing_default_86400 | 0 |
| 401 | 119.91.225.50 | unknown_task_pattern | missing_default_86400 | 0 |
| 400 | 106.53.183.89 | unknown_task_pattern | missing_default_86400 | 0 |
| 409 | 111.230.28.100 | unknown_task_pattern | missing_default_86400 | 0 |
| 398 | 114.132.95.88 | unknown_task_pattern | missing_default_86400 | 0 |
| 397 | 139.199.84.10 | unknown_task_pattern | missing_default_86400 | 0 |
| 395 | 42.193.182.190 | unknown_task_pattern | missing_default_86400 | 0 |
| 394 | 119.91.108.108 | unknown_task_pattern | missing_default_86400 | 0 |
| 399 | 139.199.172.228 | unknown_task_pattern | missing_default_86400 | 0 |
| 393 | 119.91.254.157 | unknown_task_pattern | missing_default_86400 | 0 |
| 388 | 139.199.19.233 | unknown_task_pattern | missing_default_86400 | 0 |
| 381 | 20.239.6.127 | unknown_task_pattern | missing_default_86400 | 0 |
| 389 | 106.55.92.145 | unknown_task_pattern | missing_default_86400 | 0 |
| 387 | 118.126.97.140 | unknown_task_pattern | missing_default_86400 | 0 |
| 385 | 42.193.180.53 | unknown_task_pattern | missing_default_86400 | 0 |
| 386 | 123.207.39.88 | unknown_task_pattern | missing_default_86400 | 0 |
| 396 | 175.178.110.156 | unknown_task_pattern | missing_default_86400 | 0 |
| 370 | 52.184.87.139 | unknown_task_pattern | missing_default_86400 | 0 |
| 352 | 118.195.145.210 | unknown_task_pattern | missing_default_86400 | 0 |
| 351 | 119.45.120.154 | unknown_task_pattern | missing_default_86400 | 0 |
| 346 | 119.91.56.90 | unknown_task_pattern | missing_default_86400 | 0 |
| 345 | 43.138.141.171 | unknown_task_pattern | missing_default_86400 | 0 |
| 343 | 43.139.22.128 | unknown_task_pattern | missing_default_86400 | 0 |
| 344 | 119.29.20.35 | unknown_task_pattern | missing_default_86400 | 0 |
| 342 | 124.220.63.74 | unknown_task_pattern | missing_default_86400 | 0 |
| 341 | 115.159.225.242 | unknown_task_pattern | missing_default_86400 | 0 |
| 336 | 119.91.193.11 | unknown_task_pattern | missing_default_86400 | 0 |
| 337 | 106.55.106.122 | unknown_task_pattern | missing_default_86400 | 0 |
| 338 | 193.112.168.105 | unknown_task_pattern | missing_default_86400 | 0 |
| 335 | 43.136.17.135 | unknown_task_pattern | missing_default_86400 | 0 |
| 306 | 139.199.186.231 | unknown_task_pattern | missing_default_86400 | 0 |
| 305 | 175.178.234.26 | unknown_task_pattern | missing_default_86400 | 0 |
| 304 | 129.204.131.206 | unknown_task_pattern | missing_default_86400 | 0 |
| 300 | 118.126.97.2 | unknown_task_pattern | missing_default_86400 | 0 |
| 296 | 1.12.55.146 | unknown_task_pattern | missing_default_86400 | 0 |
| 299 | 134.175.220.13 | unknown_task_pattern | missing_default_86400 | 0 |
| 295 | 175.178.3.33 | unknown_task_pattern | missing_default_86400 | 0 |
| 303 | 159.75.74.94 | unknown_task_pattern | missing_default_86400 | 0 |
| 279 | 42.193.183.168 | unknown_task_pattern | missing_default_86400 | 0 |
| 280 | 43.136.121.184 | unknown_task_pattern | missing_default_86400 | 0 |
| 273 | 159.75.14.33 | unknown_task_pattern | missing_default_86400 | 0 |
| 265 | 106.55.172.50 | unknown_task_pattern | missing_default_86400 | 0 |
| 260 | 106.52.124.36 | unknown_task_pattern | missing_default_86400 | 0 |
| 258 | 81.71.154.172 | unknown_task_pattern | missing_default_86400 | 0 |
| 259 | 134.175.237.241 | unknown_task_pattern | missing_default_86400 | 0 |
| 256 | 114.132.46.237 | unknown_task_pattern | missing_default_86400 | 0 |
| 255 | 119.91.37.28 | unknown_task_pattern | missing_default_86400 | 0 |
| 254 | 42.193.215.87 | unknown_task_pattern | missing_default_86400 | 0 |
| 257 | 43.136.98.34 | unknown_task_pattern | missing_default_86400 | 0 |
| 253 | 43.139.204.116 | unknown_task_pattern | missing_default_86400 | 0 |
| 246 | 43.136.118.134 | unknown_task_pattern | missing_default_86400 | 0 |
| 245 | 193.112.191.239 | unknown_task_pattern | missing_default_86400 | 0 |
| 218 | 118.31.51.101 | unknown_task_pattern | 86400 | 0 |
| 175 | 8.163.42.25 | unknown_task_pattern | 86400 | 0 |
| 168 | 39.108.112.104 | unknown_task_pattern | 86400 | 0 |
| 167 | 120.76.53.162 | unknown_task_pattern | 86400 | 0 |
| 147 | 119.29.250.198 | unknown_task_pattern | missing_default_86400 | 0 |
| 148 | 42.194.218.55 | unknown_task_pattern | missing_default_86400 | 0 |
| 136 | 119.91.140.22 | unknown_task_pattern | missing_default_86400 | 0 |
| 135 | 159.75.238.16 | unknown_task_pattern | missing_default_86400 | 0 |
| 123 | 47.106.77.23 | unknown_task_pattern | 86400 | 0 |
| 112 | 39.108.77.77 | unknown_task_pattern | 86400 | 0 |
| 117 | 8.163.108.1 | unknown_task_pattern | 86400 | 0 |
@@ -0,0 +1,425 @@
# Nexus 宝塔长期掉线修复与测试部署记录(2026-07-07)
## 1. 当前结论
本轮重点处理的是:宝塔面板“一键登录成功后,多个面板同时打开,约 1-2 小时后刷新陆续掉线”的问题。
当前状态:
- Nexus 测试部署机:`192.168.124.219`
- Nexus 项目目录:`/opt/nexus-dev-current`
- Git 分支:`audit/security-review`
- 最新提交:`99edd30 chore: default compose ports for nexus test deploy`
- 宝塔长期会话修复提交:`a538358 fix: keep bt panel sessions alive after one-click login`
- Docker 服务已恢复:
- Nexus`0.0.0.0:18600 -> 8600`
- MySQL`0.0.0.0:13306 -> 3306`
- Redis`0.0.0.0:16379 -> 6379`
- Health check`http://127.0.0.1:18600/health` 返回 `ok`
- 容器内确认补丁已生效:
- `_SESSION_CLEANUP_TTL_CHECK_SECONDS = 86400`
- `_SESSION_CLEANUP_TTL_FAILED_RETRY_SECONDS = 3600`
---
## 2. 用户现象与根因判断
用户反馈:
> 不是登录失败,是登录宝塔以后几个小时后自动退出;一键登录同时打开 10 个宝塔,过 2 个小时陆续掉;刷新就掉线。
这个现象不像是 Nexus 生成登录 URL 失败,也不像 tmp_token 本身立即失效。更符合下面链路:
1. Nexus 一键登录宝塔时生成临时登录 token。
2. 用户成功进入宝塔面板。
3. 之后是否持续在线,取决于宝塔面板内部 session 过期/清理逻辑。
4. 如果宝塔面板仍然按老逻辑清理 session,例如 `task.py` 中固定 `if f_time > 3600:`,那么即使 Nexus 生成的 tmp_token TTL 是 86400 秒,登录后的 session 仍可能在约 1 小时左右被宝塔清理。
5. 用户刷新页面时,宝塔发现 session 已被清理,于是表现为掉线。
因此,本轮修复重点不是“能不能生成一键登录 URL”,而是:
- 确保已配置过宝塔 API 的服务器,也会在一键登录前做一次 session cleanup TTL 检查/修复。
- 避免老服务器因为已经配置过 API 而跳过 bootstrap,导致宝塔 session 清理阈值仍停留在 3600 秒。
---
## 3. 本次代码改动摘要
### 3.1 `server/application/services/btpanel_service.py`
新增/调整逻辑:
- 新增检查周期:
- `_SESSION_CLEANUP_TTL_CHECK_SECONDS = 24 * 3600`
- `_SESSION_CLEANUP_TTL_FAILED_RETRY_SECONDS = 3600`
- 在 `create_login_url()` / `_create_login_url_fresh()` 的一键登录链路中,针对已经配置过宝塔 API 的服务器,也会 best-effort 触发 session cleanup TTL 检查。
- 检查成功后 24 小时内不重复检查。
- 检查失败后 1 小时内不反复重试,避免频繁 SSH。
- 无 SSH 授权、缺中心 IP、SSH 失败时,不阻断用户登录,只记录状态。
### 3.2 `server/infrastructure/btpanel/ssh_bootstrap.py`
增强宝塔 bootstrap 输出:
- `session_cleanup_ttl_ok`
- `session_cleanup_ttl_patched`
- warnings 中记录 patch 失败或异常信息。
宝塔侧修复目标是将固定 3600 秒清理逻辑改成读取面板 session timeout
```python
session_cleanup_timeout = int(public.get_session_timeout() or 86400)
if f_time > session_cleanup_timeout:
...
```
### 3.3 `server/infrastructure/btpanel/bootstrap_state.py`
公开并记录 session cleanup 状态字段:
- `bootstrap_last_warnings`
- `session_cleanup_ttl_ok`
- `session_cleanup_ttl_checked_at`
- `session_cleanup_ttl_last_error`
- `session_cleanup_ttl_patched`
这些字段方便后续在后端/前端展示每台宝塔服务器是否已经做过长期 session 修复。
### 3.4 `tests/test_btpanel_login_url.py`
新增测试:
- 已配置宝塔 API 的服务器,一键登录前会执行一次 session cleanup TTL 检查。
- 第二次一键登录不会在 24 小时窗口内重复执行。
- 登录 URL 仍然是 fresh 生成,不缓存一次性 URL。
### 3.5 `docker-compose.yml`
新增防复发修复:
- 默认 MySQL 发布端口从 `3306` 改为 `13306`
- 默认 Redis 发布端口从 `6379` 改为 `16379`
- 默认 Nexus 发布端口从 `8600` 改为 `18600`
- 默认 CORS/API base 改到 `18600`
- Usage 注释改成推荐:
```bash
docker compose --env-file docker/.env up -d --build
```
修复原因:Docker Compose 的 `env_file` 只会传给容器环境,不一定参与 compose 变量插值;如果忘记 `--env-file docker/.env`,端口会落回默认值。之前默认值是 `3306/6379/8600`,会和测试机上的宝塔 Redis 等服务冲突。
---
## 4. 宝塔一键登录调用链
```mermaid
flowchart TD
UI["前端:一键登录按钮"] --> API["POST /api/btpanel/servers/{server_id}/login-url"]
API --> SVC["BtPanelService.create_login_url"]
SVC --> LOCK["Redis login_url_lock(server_id)"]
LOCK --> FRESH["_create_login_url_fresh"]
FRESH --> CHECK["_ensure_session_cleanup_ttl_for_login"]
CHECK --> NEEDSSH{"需要/允许 SSH 检查?"}
NEEDSSH -- "否" --> APIURL["_try_login_url_via_api"]
NEEDSSH -- "是" --> SSHBOOT["ssh_bootstrap_panel"]
SSHBOOT --> PATCH["ensure_session_cleanup_ttl patch 宝塔 task.py"]
PATCH --> STATE["apply_bootstrap_success 写入 extra_attrs.bt_panel"]
STATE --> APIURL
APIURL --> BTAPI["宝塔 API: /config?action=set_temp_login"]
BTAPI --> TOKEN["生成 tmp_token, TTL 86400"]
TOKEN --> RESOLVE["resolve_temp_login_url"]
RESOLVE --> URL["返回登录 URL"]
APIURL -- "API 不可用" --> SSHLOGIN["ssh_temp_login_url fallback"]
SSHLOGIN --> URL
```
关键点:
- Redis lock 防止同一台服务器并发点击互相覆盖 tmp_token。
- tmp_token TTL 是 86400 秒。
- 本次补上的是“登录后 session cleanup TTL”的检查,解决成功进入宝塔后过一段时间刷新掉线的问题。
---
## 5. 已验证测试
在测试机 `/opt/nexus-dev-current` 上通过:
```bash
.venv/bin/python -m pytest tests/test_btpanel_login_url.py tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_ssh_bootstrap.py -q
```
结果:
```text
25 passed
```
完整后端测试:
```bash
.venv/bin/python -m pytest -q
```
结果:
```text
728 passed, 1 skipped
```
运行容器验证:
```text
Nexus health: ok
SESSION_CLEANUP_TTL_CHECK_SECONDS: 86400
FAILED_RETRY_SECONDS: 3600
```
---
## 6. 测试机 Docker 状态
当前容器状态:
```text
nexus-nexus-1 Up healthy 0.0.0.0:18600->8600/tcp
nexus-mysql-1 Up healthy 0.0.0.0:13306->3306/tcp
nexus-redis-1 Up healthy 0.0.0.0:16379->6379/tcp
```
宿主机宝塔 Redis
```text
127.0.0.1:6379
```
已避免 Nexus Redis 占用宿主机 `6379`
---
## 7. Docker 镜像构建代理问题
本轮执行 `docker compose --env-file docker/.env up -d --build` 时,Docker build 阶段失败过一次:
```text
proxyconnect tcp: dial tcp 127.0.0.1:10808: connect: connection refused
```
原因:测试机上的 Docker daemon 使用了 `127.0.0.1:10808` 代理;但这里的 `127.0.0.1` 指的是 Linux 测试机自己,不是 Windows 主机。如果 Linux 测试机本地没有运行代理服务,Docker 就无法通过这个地址拉镜像。
本轮恢复方式:
- 使用测试机已有的本地镜像 `nexus-nexus:latest`
- 执行 `docker compose --env-file docker/.env up -d --no-build` 恢复服务。
- 容器内确认代码已经包含本次宝塔长期会话修复。
后续建议二选一:
1. 在 Linux 测试机本地启动可用代理,并确保 Docker daemon 能访问。
2. 不使用 `127.0.0.1:10808`,改成测试机能访问的局域网代理地址,或者直接使用可用 Docker registry mirror。
---
## 8. 回滚方案
如需回滚本次宝塔长期会话修复:
```bash
cd /opt/nexus-dev-current
git revert a538358
```
如需只回滚 compose 默认端口防复发修复:
```bash
cd /opt/nexus-dev-current
git revert 99edd30
```
恢复部署推荐命令:
```bash
cd /opt/nexus-dev-current
docker compose --env-file docker/.env up -d --no-build
```
如果镜像代理问题已解决,再使用:
```bash
cd /opt/nexus-dev-current
docker compose --env-file docker/.env up -d --build
```
---
## 9. 仍需关注的边界问题
1. `_ensure_session_cleanup_ttl_for_login()` 复用了 `ssh_bootstrap_panel()`,会同时检查/修复宝塔 API token、IP 白名单和 session cleanup TTL。
- 优点:能顺便修复 API 白名单漂移。
- 风险:首次检查会增加一次 SSH 耗时。
2. `_server_has_ssh_auth()` 对 preset 的判断偏粗,只看配置里是否存在 preset id。
- 后续安全巡检要确认 preset 缺失、失效、被删时是否降级得足够安全。
3. `apply_bootstrap_success()` 现在也可能把 session cleanup 维护检查计入 `bootstrap_attempts`
- 如果后续要精细统计,可以拆出单独的 maintenance check counter。
4. Docker build 代理配置需要单独治理。
- 当前运行服务没问题,但未来如果要重新 build,需要先修 Docker daemon 代理或镜像源。
---
## 10. 后续安全巡检计划
### 阶段 1:宝塔登录链路复审
- [x] 一键登录 tmp_token TTL 检查
- [x] Redis per-server lock 检查
- [x] 已配置 API 老服务器跳过 bootstrap 的边界修复
- [x] session cleanup TTL 检查/修复
- [x] 单测与完整后端测试
- [x] Docker 测试机运行验证
### 阶段 2:SSH / 命令执行风险巡检
重点文件:
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `server/infrastructure/btpanel/ssh_login.py`
- `server/application/services/server_file_transfer_service.py`
- `server/infrastructure/ssh/*`
检查项:
- 是否存在 shell 注入风险。
- 是否所有远程命令都有 timeout。
- 是否所有用户输入进入命令前都做 quote/escape。
- 是否有敏感信息进入日志。
- 是否有 sudo / root 权限边界问题。
### 阶段 3:API 鉴权与权限边界巡检
重点:
- `server/api/*.py`
- 依赖注入中的 `Depends` / 当前用户获取逻辑
- API key / session / CSRF / CORS
检查项:
- 是否存在未鉴权管理接口。
- 是否存在越权访问其他服务器资源。
- 是否存在批量操作缺少权限校验。
- CORS 是否过宽。
### 阶段 4:数据库与敏感字段巡检
重点:
- `server/models/*`
- `server/repositories/*`
- `server/application/services/*`
检查项:
- 密码、token、key 是否加密存储。
- 查询是否存在 SQL 注入风险。
- extra_attrs 是否可能被污染或写入不可控结构。
- 数据迁移是否兼容老数据。
### 阶段 5Redis / 锁 / 并发巡检
重点:
- 登录 URL 锁
- bootstrap 锁
- 批量 bootstrap 并发
- 缓存失效策略
检查项:
- 锁 TTL 是否足够。
- 异常时锁是否释放。
- 并发点击是否导致 token 覆盖。
- 多 worker/多容器下是否一致。
---
## 11. 当前建议
短期:
1. 用当前部署继续测试“一键登录同时打开 10 个宝塔,2 小时后刷新是否还掉线”。
2. 优先测试之前最容易掉线的服务器,例如用户提到的 `42.193.182.190`
3. 如果仍掉线,下一步要查看该目标宝塔机器的 session cleanup patch 状态字段,而不是只看 Nexus 登录 URL 是否生成成功。
中期:
1. 修 Docker daemon 代理,保证以后可稳定 `--build`
2. 把 session cleanup 状态展示到 Nexus 后台,方便看到每台宝塔是否已修复。
3. 继续做 SSH/API/DB/Redis 安全巡检。
---
## 12. SSH/文件管理安全巡检阶段 1 补充(2026-07-07
用户已确认:`script_service` 是设计内“管理员远程执行命令”能力,天然允许任意 shell,不作为漏洞修复项。后续仅检查其鉴权、审计、敏感信息日志边界。
本轮继续修复非脚本服务的命令参数边界:
- `112e617 fix: stop archive option injection via filenames`
- `remote_archive.py` 的 tar/zip 压缩命令增加 `--` 参数终止符。
- `bcec78f fix: harden file manager shell operations`
- 文件管理 `rm/mv/mkdir/cp` 命令增加 `--` 参数终止符。
- 递归 chmod/chown 禁止关键系统目录子树,例如 `/etc/nginx``/usr/*``/var/log/*`
- 仍允许常见 Web 根目录,例如 `/www/wwwroot/site``/var/www/html`
验证:
```text
pytest tests/test_file_permissions.py tests/test_schema_path_validators.py tests/test_remote_archive_commands.py -q
22 passed in 0.54s
pytest -q
733 passed, 1 skipped in 11.77s
```
运行容器已热更新并重启,`http://127.0.0.1:18600/health` 返回 `ok`
---
## 13. SSH/文件管理安全巡检阶段 2:安全解压(2026-07-07
新增提交:
```text
845d7e2 fix: validate remote archives before extraction
```
修复内容:
- `/api/sync/decompress` 不再直接执行 `tar xzf` / `unzip -o`
- 新增 `run_remote_decompress()`:先列归档成员,再校验,再解压。
- 拒绝归档内部:
- 绝对路径,例如 `/etc/passwd`
- 路径穿越,例如 `../evil.txt`
- Windows 反斜杠路径,例如 `dir\evil.txt`
- 符号链接、硬链接、设备文件、FIFO、socket
- tar 解压增加:
```bash
tar --no-same-owner --no-same-permissions -xzf <archive> -C <dest>
```
验证:
```text
pytest tests/test_remote_archive_commands.py tests/test_schema_path_validators.py tests/test_file_permissions.py -q
31 passed in 0.55s
pytest -q
742 passed, 1 skipped in 11.83s
```
运行容器已热更新并重启,`http://127.0.0.1:18600/health` 返回 `ok`
@@ -0,0 +1,123 @@
# Nexus 新增宝塔 Session 自动检测/修复记录
日期:2026-07-07
测试部署机:192.168.124.219
项目目录:/opt/nexus-dev-current
分支:audit/security-review
提交:5ac3f56 fix: auto repair btpanel session cleanup ttl
## 目标
1. 新增服务器/新增宝塔自动检测宝塔 `/www/server/panel/task.py` 是否存在 `if f_time > 3600:` 的 session 清理硬编码。
2. 只在识别到精确可修复模式时自动备份并 patch。
3. 一键登录前增加兜底检测/修复,避免已经配置过 API Key 的老服务器继续因为 1 小时清理而刷新掉线。
4. 把检测/修复状态保存到 `servers.extra_attrs.bt_panel`,并在登录 URL 响应 diagnostics 里返回非敏感诊断信息。
## 修改文件
- `/opt/nexus-dev-current/server/infrastructure/btpanel/ssh_bootstrap.py`
- `/opt/nexus-dev-current/server/infrastructure/btpanel/bootstrap_state.py`
- `/opt/nexus-dev-current/server/application/services/btpanel_service.py`
- `/opt/nexus-dev-current/tests/test_btpanel_login_url.py`
- `/opt/nexus-dev-current/tests/test_btpanel_ssh_bootstrap.py`
## 核心逻辑
### 新增/绑定宝塔时
`ssh_bootstrap_panel()` 远程执行 bootstrap 脚本时会:
1. 检查 `/www/server/panel/task.py`
2. 如果已经包含 `session_cleanup_timeout = int(public.get_session_timeout() or 86400)` 且清理条件为 `if f_time > session_cleanup_timeout:`,记录为 `patched_timeout`
3. 如果命中旧模式:
- 备份到 `/www/server/panel/task.py.nexus-session-ttl.bak`
- 将固定 3600 秒改成读取 `public.get_session_timeout()`
- 保留原文件权限;
- 先执行 Python compile 和 `py_compile.compile(tmp, doraise=True)`
- 原子替换 task.py
- 重载宝塔 `/etc/init.d/bt reload`
4. 如果 task.py 结构不匹配,记录 `unknown_task_pattern`,不盲改。
### 一键登录前
`BtPanelService.create_login_url()` 在生成宝塔一键登录 URL 前会:
1. 若服务器已配置宝塔 API,则调用 `_ensure_session_cleanup_ttl_for_login()`
2. 该检查按缓存节流:
- 成功后 24 小时内不重复修复;
- 失败后 1 小时后可重试。
3. 修复失败不会阻断本次一键登录,只会记录 diagnostics 和状态。
## 新增状态字段
保存到:`servers.extra_attrs.bt_panel`
- `session_cleanup_ttl_ok`
- `session_cleanup_ttl_patched`
- `session_cleanup_task_state`
- `session_cleanup_ttl_checked_at`
- `session_cleanup_ttl_last_error`
- `panel_ssl`
`session_cleanup_task_state` 当前可能值:
- `patched_timeout`:已经修好或本次修好;
- `unknown_task_pattern`:task.py 存在,但不是已知可安全 patch 的结构;
- `no_panel`:未找到 task.py
- `error`:检查/修复异常。
## 安全边界
- 不保存、不输出宝塔 tmp_token。
- 不显示 API Key 明文。
- 不自动修改 `panel_ssl=0`
- 不自动改 `check_client_hash()`
- 不对未知 task.py 结构做模糊替换。
- 修改 task.py 前一定备份,写入前做编译检查,写入使用原子替换。
## 验证
在测试部署机 `/opt/nexus-dev-current` 执行:
```bash
.venv/bin/python -m py_compile \
server/infrastructure/btpanel/ssh_bootstrap.py \
server/infrastructure/btpanel/bootstrap_state.py \
server/application/services/btpanel_service.py \
tests/test_btpanel_login_url.py \
tests/test_btpanel_ssh_bootstrap.py
.venv/bin/python -m pytest \
tests/test_btpanel_ssh_bootstrap.py \
tests/test_btpanel_login_url.py \
tests/test_btpanel_bootstrap_loop.py \
tests/test_btpanel_login_url_route.py \
tests/test_btpanel_ssh_login.py \
tests/test_btpanel_temp_login_ttl.py -q
```
结果:
```text
39 passed in 0.61s
```
测试容器已热更新并重启:
```text
curl http://127.0.0.1:18600/health => ok
nexus-nexus-1 healthy
```
注意:测试机 `docker compose up -d --build nexus` 曾因为远端 docker 代理 `127.0.0.1:10808` 不可达导致 metadata 拉取失败,所以本次测试环境采用 `docker cp + docker restart` 热更新容器。代码已提交到工作树,后续可在代理可用时重建镜像。
## 后续发布正式环境建议
正式环境不要 `git reset`,建议继续采用之前方式:
1. 备份正式容器内对应文件;
2. `docker cp` 本次 3 个后端文件到正式容器;
3. 容器内 `python -m py_compile`
4. restart 容器;
5. `/health` 检查;
6. 抽一台新增/未修复宝塔做一键登录验证,确认 diagnostics 返回 `session_cleanup_task_state=patched_timeout`
@@ -0,0 +1,246 @@
# Nexus 最新代码上下文索引与 Zvec 部署报告
生成时间:2026-07-07
测试机:`192.168.124.219`
本地工作区:`C:\Users\uzuma\Documents\Codex\2026-07-06\yuu`
---
## 1. 已完成结论
已处理完成:
1. 已从 Nexus 测试机同步最新代码快照,并重建代码上下文索引。
2. 已清理上下文索引污染源:本地快照中误带入的 `.megamemory/knowledge.db` 已删除。
3. 已确认本地上下文库没有 `.db/.sqlite/.sqlite3/.duckdb/.faiss` 文件残留。
4. 已确认最新快照没有敏感命名文件纳入索引,例如 `.env``.pem``.key``secret``resolved``凭据` 等。
5. 已增强索引脚本脱敏规则:排除 `.megamemory/vendor` 等目录,并对 PEM 私钥块做整体脱敏。
6. 已在测试机安装 Zvec:独立安装在 `/opt/nexus-code-context/.venv`,不改 Nexus 主项目虚拟环境。
7. 已建立 Zvec 本地代码向量库:`/opt/nexus-code-context/data/nexus_code_chunks.zvec`
8. 已导入清理后的脱敏 chunks:`1440` 条,导入失败数 `0`
9. 已验证 Zvec 向量检索可查到宝塔一键登录/session 相关代码。
10. 没有使用 SQLite 作为新的上下文数据库;结构化索引仍是 JSONL/Markdown,向量库是 Zvec。
---
## 2. 最新上下文索引统计
来源快照:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
```
统计:
| 项 | 数量 |
|---|---:|
| 文件总数 | 2214 |
| 文本索引文件 | 2020 |
| Python/前端符号 | 2861 |
| API 路由 | 214 |
| Service 方法 | 324 |
| 调用边 | 16780 |
| 数据库调用点 | 437 |
| Redis key/调用点 | 729 |
| SSH/远程命令调用点 | 346 |
| 宝塔相关流 | 183 |
| 前端路由 | 31 |
| 脱敏代码块 | 1440 |
| 生成时间 | 2026-07-07 11:47:00 +0800 |
本地索引目录:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context
```
主要产物:
```text
work/nexus_code_context/context_summary.json
work/nexus_code_context/index/api_routes.jsonl
work/nexus_code_context/index/service_methods.jsonl
work/nexus_code_context/index/call_edges.jsonl
work/nexus_code_context/index/db_queries.jsonl
work/nexus_code_context/index/redis_keys.jsonl
work/nexus_code_context/index/ssh_commands.jsonl
work/nexus_code_context/index/btpanel_flows.jsonl
work/nexus_code_context/cache/chunks.redacted.jsonl
work/nexus_code_context/reports/*.md
work/nexus_code_context/scripts/build_context_index.py
work/nexus_code_context/scripts/query_context.py
work/nexus_code_context/scripts/import_chunks_to_zvec.py
work/nexus_code_context/scripts/query_zvec_context.py
```
---
## 3. SQLite / 污染处理结果
用户之前明确不喜欢 SQLite,也担心上下文库污染。处理结果:
### 3.1 已发现并清理的问题
本地最新源码快照中曾误包含:
```text
work/nexus_code_context/snapshots/.../.megamemory/knowledge.db
```
该文件属于项目外部记忆/缓存目录,不应进入 Nexus 代码上下文库。已删除,并在索引脚本中加入排除:
```python
.megamemory
.cursor
.playwright-mcp
.skills
vendor
```
### 3.2 当前检查结果
当前本地上下文目录检查:
```text
.db/.sqlite/.sqlite3/.duckdb/.faiss: 0 个
敏感命名文件: 0 个
PEM 私钥块命中: 0 个
JWT 样式 token 命中: 0 个
AWS AKIA key 命中: 0 个
未脱敏 password/token/key 赋值命中: 0 个
```
因此当前上下文库不再包含 SQLite 数据库,也没有发现上述明显 secret 形态污染。
---
## 4. Zvec 安装结果
Zvec 没有装进 Nexus 主项目 `.venv`,而是独立放在测试机:
```text
/opt/nexus-code-context/.venv
```
版本:
```text
zvec 0.5.1
```
Zvec 数据目录:
```text
/opt/nexus-code-context/data/nexus_code_chunks.zvec
```
Manifest
```json
{
"collection": "/opt/nexus-code-context/data/nexus_code_chunks.zvec",
"chunks_file": "/opt/nexus-code-context/input/chunks.redacted.jsonl",
"embedding": "local_hashing_vector_no_external_api",
"dimension": 256,
"inserted_attempted": 1440,
"insert_status_non_ok": 0
}
```
说明:
- 没有用 Docker 部署 Zvec,所以不会占用宝塔/Nexus 端口。
- 没有公网暴露 Zvec 服务。
- 没有调用外部 embedding API 上传私有代码。
- embedding 使用本地确定性 hashing vector,只用于代码上下文召回,不用于语义模型训练。
- 原始输入是 `chunks.redacted.jsonl`,不是未脱敏源码。
---
## 5. Zvec 查询验证
已在测试机验证向量检索:
查询:
```bash
/opt/nexus-code-context/.venv/bin/python /opt/nexus-code-context/bin/query_zvec_context.py 'bt panel session cleanup keep alive one click login' --topk 5 --mode vector
```
返回命中包括:
```text
server/infrastructure/btpanel/constants.py
server/infrastructure/btpanel/login_url_lock.py
server/application/services/btpanel_service.py
```
这说明当前 Zvec 代码上下文库可以召回宝塔一键登录/session 保活相关代码。
---
## 6. 后续使用命令
### 6.1 本地重建 JSONL/Markdown 索引
在本地工作区执行:
```powershell
$env:PYTHONIOENCODING='utf-8'
python work/nexus_code_context/scripts/build_context_index.py
```
### 6.2 重新导入测试机 Zvec
先把清理后的 chunks 和脚本同步到测试机,再执行:
```bash
/opt/nexus-code-context/.venv/bin/python /opt/nexus-code-context/bin/import_chunks_to_zvec.py --chunks /opt/nexus-code-context/input/chunks.redacted.jsonl --collection /opt/nexus-code-context/data/nexus_code_chunks.zvec --recreate
```
### 6.3 查询 Zvec 上下文库
```bash
/opt/nexus-code-context/.venv/bin/python /opt/nexus-code-context/bin/query_zvec_context.py '你的问题关键词' --topk 8 --mode vector
```
示例:
```bash
/opt/nexus-code-context/.venv/bin/python /opt/nexus-code-context/bin/query_zvec_context.py 'archive extract tar zip path traversal' --topk 8 --mode vector
```
---
## 7. 对 Nexus 代码审查的帮助
现在的上下文库可以辅助后续安全巡检:
1. 通过 `api_routes.jsonl` 快速定位 API 入口。
2. 通过 `service_methods.jsonl` 找业务服务边界。
3. 通过 `call_edges.jsonl` 拼 API → Service → Repo/SSH/Redis 调用链。
4. 通过 `db_queries.jsonl` 聚合 SQLAlchemy 查询和写入点。
5. 通过 `ssh_commands.jsonl` 聚合远程 shell/SSH/SFTP/文件管理风险点。
6. 通过 `btpanel_flows.jsonl` 专门追踪宝塔一键登录、session、token、保活修复相关代码。
7. 通过 Zvec 对关键词/问题做代码块召回,先定位上下文,再做人工审查。
---
## 8. 当前限制
1. 当前 Zvec 检索优先使用本地 hashing vector。它稳定、离线、不上传代码,但语义能力弱于大模型 embedding。
2. Zvec FTS 字段已按 schema 配置,但当前实际使用以 vector 模式为主;后续如果要做精确全文检索,可再单独调优 FTS index/tokenizer。
3. 上下文库只用于辅助定位,不替代测试和人工安全审查。
---
## 9. 下一步建议
建议后续 Nexus 安全巡检按这个顺序推进:
1. 先用 `api_routes.jsonl` + `service_methods.jsonl` 画全量 API/服务/数据库调用链。
2. 对 `btpanel_flows.jsonl` 做二次审查,重点检查:token TTL、并发一键登录、Redis lock、审计日志、失败回退。
3. 对 `ssh_commands.jsonl` 做逐项确认:鉴权、超时、输出截断、日志脱敏、危险 shell 参数边界。
4. 对 `db_queries.jsonl` 检查多租户/权限过滤、分页限制、事务提交/回滚。
5. 每次修复后重新跑索引和 Zvec 导入,确保上下文库跟代码同步。
@@ -0,0 +1,109 @@
# Nexus 正式环境遗漏补发记录(2026-07-07)
## 背景
用户反馈:正式页面里“全局 API Key / API Key”仍可见。排查后确认:此前正式环境只热更新了宝塔 diagnostics 相关后端文件,`524acd5 fix-stop-exposing-global-api-key-in-settings``fa44439 fix btpanel ssh temp login python env` 未完整补发到正式容器。
## 本次补发范围
来源:局域网测试机 `/opt/nexus-dev-current`
补发到正式容器 `nexus-prod-nexus-1` 的文件:
- `server/api/settings.py`
- GET `/api/settings/` 对敏感设置不再返回明文值。
- `api_key` / `secret_key` / `encryption_key` / Telegram Bot Token 返回 `value=""` + `value_set=true/false`
- `server/infrastructure/btpanel/ssh_login.py`
- 补发 SSH 临时登录 Python 环境兼容修复。
- `web/app/index.html`
- `web/app/assets/`
- 补发 SettingsPage 前端构建产物,使当前设置页不再展示旧的全局 API Key 明文字段。
## 补丁包
测试机生成:
- `/tmp/nexus-settings-frontend-patch-20260707.tar.gz`
- SHA256`a56153c16ca74a80597ffd134bed3aa2bcdcef1c77b7e67a0998bdd1b85a13d6`
本地留存:
- `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-settings-frontend-patch-20260707.tar.gz`
正式机上传:
- `/tmp/nexus-settings-frontend-patch-20260707.tar.gz`
## 正式环境热更新结果
正式主机:`20.24.218.235`
容器:`nexus-prod-nexus-1`
执行动作:
1. 校验补丁包 SHA256。
2. 解压到 staging 目录。
3. 备份容器旧文件。
4. `docker cp` 覆盖后端与前端构建产物。
5. `python -m py_compile` 检查两个 Python 文件语法。
6. 重启容器。
7. 访问 `/health` 健康检查。
结果:
- 备份目录:`/tmp/nexus-settings-patch-backup-20260707-170246`
- staging 目录:`/tmp/nexus-settings-patch-20260707-170246`
- 健康检查:`ok`
## 验证结果
### 1. API 不再返回全局 API Key 明文
通过正式域名 `https://api.synaglobal.vip` 登录后调用:
- `GET /api/settings/`
验证结果:
- 登录:`HTTP/1.1 200 OK`
- Settings`HTTP/1.1 200 OK`
- settings_count`33`
- sensitive_keys_present`api_key`, `secret_key`, `telegram_bot_token`, `telegram_offline_bot_token`
- sensitive_values_plaintext`NO`
- `api_key_value_set``True`
说明:正式 API 仍会告诉前端“已配置过 API Key”,但不会返回真实值。
### 2. 当前前端入口已更新
正式容器当前入口:
- `/app/web/app/index.html`
- 引用入口 JS`/app/assets/index-CASoZdpL.js`
- 当前 SettingsPage 构建文件:`SettingsPage-DFgw8KT9.js`
检查当前 SettingsPage 构建文件未发现 `API` 文案输出;目录内旧的 `SettingsPage-*.js` 残留不代表正在被当前入口使用。
## 用户侧注意事项
如果浏览器还看到旧的“全局 API Key / API Key”区块,优先按以下顺序处理:
1. 强制刷新页面(Windows`Ctrl + F5`)。
2. 退出 Nexus 后重新登录。
3. 清除 `api.synaglobal.vip` 站点缓存。
4. 确认访问的是正式域名 `https://api.synaglobal.vip`,不是本地 `http://localhost:18600/app/` 旧代理页面。
## 回滚方式
如需回滚本次热更新,可从正式机备份目录恢复:
- `/tmp/nexus-settings-patch-backup-20260707-170246/settings.py`
- `/tmp/nexus-settings-patch-backup-20260707-170246/ssh_login.py`
- `/tmp/nexus-settings-patch-backup-20260707-170246/web-app`
恢复后重启容器并检查 `/health`
## 结论
本次已经把之前遗漏的正式环境文件补发完成。当前正式 API 不再泄露全局 API Key 明文;前端当前入口也已切到新的 SettingsPage 构建产物。
@@ -0,0 +1,292 @@
# Nexus 测试部署机环境修复与验证报告
生成时间:2026-07-07 06:55Asia/Shanghai
## 1. 本轮结论
Nexus 局域网测试部署机 `192.168.124.219` 当前可作为测试部署机使用:
- Docker 部署正常,Nexus / MySQL / Redis 三个容器均为 healthy。
- Nexus 健康接口正常:`http://127.0.0.1:18600/health` 返回 `200 OK` / `ok`
- Web 入口正常:`http://192.168.124.219:18600/app/`
- 后端全量 pytest 已通过:`727 passed, 1 skipped in 11.75s`
- 前端 `type-check``build` 已通过。
- 已确认机器已使用国内源,未执行 `linuxmirrors.cn` 换源脚本,避免重复改源带来风险。
## 2. 当前部署状态
部署目录:
```text
/opt/nexus-dev-current -> /opt/nexus-dev-20260707-054832
```
Docker 端口:
| 服务 | 容器端口 | 宿主端口 | 状态 |
|---|---:|---:|---|
| Nexus | 8600 | 18600 | healthy |
| MySQL | 3306 | 13306 | healthy |
| Redis | 6379 | 16379 | healthy |
测试机上同时存在宝塔服务,本轮没有修改宝塔文件,也没有重启宝塔:
| 服务 | 端口 |
|---|---:|
| 宝塔面板 | 29938 |
| 宝塔 nginx | 80 / 888 |
## 3. 国内源与代理状态
已检查到当前测试机已配置国内源:
- Ubuntu apt:阿里云源。
- Docker CE:腾讯云源。
- pip:清华源。
- npm`https://registry.npmmirror.com`
- Docker registry mirror`https://docker.1ms.run/`
Docker daemon 代理已配置为:
```text
HTTP_PROXY=socks5h://127.0.0.1:10808
HTTPS_PROXY=socks5h://127.0.0.1:10808
```
注意:这个 Docker 代理依赖本机到测试机的 SSH reverse tunnel;如果隧道断开,已存在镜像和容器不受影响,但后续拉新镜像可能失败。
## 4. 本轮已修复文件
所有上传到远端的文件均先在远端做了 `.bak-时间戳` 备份。
### 4.1 之前已上传的环境/测试修复
```text
/opt/nexus-dev-current/pytest.ini
/opt/nexus-dev-current/tests/conftest.py
/opt/nexus-dev-current/tests/test_agent_version.py
/opt/nexus-dev-current/tests/test_agent_heartbeat_stop.py
/opt/nexus-dev-current/tests/test_btpanel_login_url.py
/opt/nexus-dev-current/server/infrastructure/ssh/remote_probe.py
/opt/nexus-dev-current/tests/test_server_connectivity.py
```
主要修复点:
1. 新增 `pytest.ini`,限制 pytest 只收集 `tests/`,避免误收集 `scripts/_browse_test.py`
2. `tests/conftest.py` 默认把普通测试设置为已安装模式,避免裸跑 pytest 时进入安装模式导致 API 返回 503。
3. 测试环境缺少 `ENCRYPTION_KEY` 时注入固定测试 Fernet key,避免测试依赖真实部署 secrets。
4. Agent 版本测试从旧的 `2.0.0` 更新为当前代码版本 `2.1.1`
5. 宝塔一键登录 URL 单测更新为“不缓存一次性 tmp_token URL”,符合近期长期掉线修复方向。
6. `remote_probe.py` 对缺少 `psutil` 的目标机增加 `/proc` 降级采集,避免监控命令直接失败。
7. `test_server_connectivity.py` 修复 mock 行字段数不足的问题。
### 4.2 本轮继续上传的链路测试修复
本轮新增上传:
```text
/opt/nexus-dev-current/tests/chain/test_batch_job_flow.py
/opt/nexus-dev-current/tests/chain/test_script_exec_flow.py
```
远端备份时间戳:
```text
20260707-064723
```
修复原因:
两个链路测试原来 monkeypatch 了全局 `asyncio.create_task`,导致 SQLAlchemy / aiosqlite 在测试 teardown 关闭连接时也拿到 `MagicMock`,最终报错:
```text
TypeError: An asyncio.Future, a coroutine or an awaitable is required
RuntimeWarning: coroutine 'AsyncConnection.close' was never awaited
```
修复方式:
- 不再污染全局 `asyncio` 模块。
- 只替换业务模块里的 `asyncio` 引用。
- 用 `_AsyncioProxy` 保留其他 `asyncio` 能力,只拦截业务代码创建后台任务的 `create_task()`
示意:
```python
class _AsyncioProxy:
def __getattr__(self, name):
return getattr(asyncio, name)
def create_task(self, coro):
return capture_create_task(coro)
monkeypatch.setattr(
"server.application.services.server_batch_service.asyncio",
_AsyncioProxy(),
)
```
另一个文件同理替换:
```python
monkeypatch.setattr(
"server.application.services.script_service.asyncio",
_AsyncioProxy(),
)
```
## 5. 验证结果
### 5.1 本轮 targeted 测试
命令:
```bash
cd /opt/nexus-dev-current
. .venv/bin/activate
python -m pytest -q \
tests/chain/test_batch_job_flow.py::test_batch_category_job_completes_and_persists \
tests/chain/test_script_exec_flow.py::test_execute_script_reaches_completed
```
结果:
```text
2 passed in 0.97s
```
### 5.2 后端全量 pytest
命令:
```bash
cd /opt/nexus-dev-current
. .venv/bin/activate
python -m pytest -q
```
结果:
```text
727 passed, 1 skipped in 11.75s
```
### 5.3 Docker / HTTP 健康检查
检查时间:2026-07-07 06:48:38 +08:00
```text
nexus-mysql-1 Up / healthy 0.0.0.0:13306->3306
nexus-redis-1 Up / healthy 0.0.0.0:16379->6379
nexus-nexus-1 Up / healthy 0.0.0.0:18600->8600
```
健康接口:
```text
HTTP/1.1 200 OK
ok
```
Web 入口:
```text
HTTP/1.1 200 OK
/app/
```
### 5.4 前端检查
命令:
```bash
cd /opt/nexus-dev-current/frontend
npm run type-check -- --pretty false
npm run build
```
结果:
```text
type-check 通过
build 通过
```
存在非阻断 warning
1. `:deep(...)` CSS 伪类 warning
- 构建器提示 `deep` 不是有效 pseudo-class,建议 `::deep`
- 当前不阻断 build,但建议后续统一检查 Vue scoped CSS 写法。
2. dynamic import warning
- `src/router/index.ts` 同时被静态和动态导入,影响拆包效果,不影响功能。
3. chunk size warning
- Monaco / Vuetify 相关 bundle 较大,建议后续做按需加载和 chunk 拆分。
## 6. 宝塔长期掉线问题现状
用户描述的问题是:
> 不是登录失败,而是一键登录同时打开多个宝塔后,过几个小时陆续掉线,刷新就掉。
当前 Nexus 代码方向已经覆盖了关键点:
1. 每次一键登录 fresh 生成宝塔 tmp_token。
2. 不缓存一次性登录 URL。
3. Redis per-server lock 避免同一服务器并发生成 tmp_token 互相覆盖。
4. temp login TTL 目标为 86400 秒。
但测试机宝塔本体此前检查到仍可能存在边界风险:
```text
/www/server/panel/data/session_timeout.pl = 86400
/www/server/panel/task.py 仍有 if f_time > 3600:
```
这意味着即使 session_timeout 设置为 86400,宝塔后台某些清理逻辑仍可能按 3600 秒判断,从而造成几个小时后刷新掉线。
本轮没有修改宝塔本体文件。后续如果要验证/修复,需要明确执行以下步骤:
1. 备份 `/www/server/panel/task.py`
2. 只修改硬编码 session 清理阈值相关逻辑。
3. 尽量不重启宝塔;如必须重启,先告知影响。
4. 再做 2 小时以上多窗口一键登录保活测试。
## 7. 后续建议的代码安全巡检顺序
建议按下面顺序继续做 Nexus 安全巡检:
1. **配置与 secrets**
- 检查 `.env`、Docker env、日志输出是否泄露 token/password/key。
- 检查测试文件是否写入真实密钥。
2. **认证与授权**
- JWT 登录、安装模式、白名单、管理员接口鉴权。
- 一键登录宝塔的权限边界与审计日志。
3. **宝塔一键登录链路**
- tmp_token 生成、并发锁、TTL、错误重试、URL 不缓存策略。
- 多窗口/多服务器/刷新场景。
4. **SSH 执行链路**
- 命令拼接、参数转义、超时、输出脱敏、失败回滚。
5. **数据库访问**
- SQLAlchemy 查询是否参数化。
- 事务边界、异步 session 生命周期。
6. **Redis 缓存/锁**
- key 命名空间、TTL、锁释放、异常路径。
7. **文件/脚本执行**
- 上传路径限制、脚本内容校验、目录穿越风险。
8. **前端安全**
- XSS、URL 拼接、iframe/window.open、token 存储位置。
9. **部署安全**
- Docker 端口暴露、默认密码、CORS、安全响应头、Nginx 代理配置。
10. **回归测试补齐**
- 为本次发现的安装模式、一次性 URL、asyncio monkeypatch、psutil 降级补长期回归用例。
## 8. 当前是否还有环境问题
按本轮检查结果:
- 测试机 Docker 部署没有发现阻断问题。
- 后端 pytest 没有剩余失败。
- 前端 type-check/build 没有失败。
- 仍需关注的是前端 build warning 和宝塔本体 `task.py` 的 3600 秒清理逻辑。
@@ -0,0 +1,314 @@
# Nexus 项目说明文档(2026-07-08
## 1. 项目定位
Nexus 是一个服务器资产管理、远程运维、宝塔面板管理和自动化发布平台。当前重点能力包括:
- 服务器资产管理;
- SSH / WebSSH / 文件管理;
- 批量脚本执行;
- 宝塔面板一键登录;
- 宝塔 Session TTL 自动检测与修复;
- 审计日志与安全巡检;
- 旧后台 `/app/` 与新后台 `/app-v2/` 双后台并行;
- 发布包制品化与 Gitea / Act Runner 自动发布方向。
## 2. 当前项目路径
源码快照目录:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
```
发布文档目录:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs
```
最新发布包:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
## 3. 技术栈概览
### 后端
- Python / FastAPI 风格 API
- SQLAlchemy
- Redis
- SSH / Paramiko 相关远程能力;
- Docker / 1Panel 部署脚本;
- 宝塔 API + SSH bootstrap 混合修复链路。
### 前端
旧后台:
```text
web/app/
```
新后台:
```text
frontend-v2/
web/app-v2/
```
新后台技术栈:
- React
- TypeScript
- Vite
- Tailwind / shadcn 风格组件;
- TanStack Query
- TanStack Table
- Zustand。
### 部署/发布
- Docker / 1Panel
- `deploy/apply-release-bundle.sh`
- `deploy/preflight-release-host.sh`
- `scripts/prepare_release_artifact.py`
- `scripts/publish_release_bundle.py`
## 4. 重要目录说明
```text
server/ 后端 API、服务、基础设施代码
server/api/ FastAPI 路由层
server/application/services/ 业务服务层
server/infrastructure/ Redis、SSH、宝塔等基础设施封装
frontend-v2/ 新后台源码
web/app/ 旧后台静态产物
web/app-v2/ 新后台静态产物
deploy/ 部署、升级、回滚、1Panel 脚本
scripts/ 本地 gate、打包、发布辅助脚本
tests/ 后端和发布辅助脚本测试
outputs/ 本轮审查、发布、说明文档
```
## 5. 当前核心改动
### 5.1 宝塔长期不掉线修复
问题根因:部分宝塔版本 `task.py` 中硬编码 `3600` 秒清理 `tmp_login` session,导致 Nexus 一键登录后 1~2 小时刷新掉线。
当前修复方向:
- 登录前自动检测/修复宝塔 hardcoded `3600`
- 改为基于 `public.get_session_timeout()` 或默认 `86400`
- 新增 Redis 分布式锁,避免同一宝塔并发签发 login URL 时 tmp_token 互相覆盖;
- SSH bootstrap 增加远端 flock / mktemp / atomic replace
- 新增测试覆盖宝塔登录、TTL、bootstrap、并发锁等场景。
关键文件:
```text
server/application/services/btpanel_service.py
server/infrastructure/btpanel/login_url_lock.py
server/infrastructure/btpanel/bootstrap_lock.py
server/infrastructure/btpanel/ssh_bootstrap.py
server/api/btpanel.py
```
### 5.2 `/app-v2` 第二后台
新增第二后台 `/app-v2/`,与旧后台 `/app/` 并行,方便逐步替换旧后台。
关键特性:
- 更现代的 UI
- 响应更快;
- 保留原功能入口;
- 敏感信息不展示;
- 全局 API Key 只显示“已配置/未配置”,不显示真实值;
- 前端构建产物有敏感扫描。
关键文件:
```text
frontend-v2/
web/app-v2/
frontend-v2/scripts/scan-app-v2-sensitive.mjs
server/main.py
Dockerfile
Dockerfile.prod
```
### 5.3 发布制品化
当前发布不再只靠零散文件复制,而是生成完整 tar.gz 发布包。
关键脚本:
```text
scripts/prepare_release_artifact.py 一键本地 gate + manifest + tar.gz + scan + metadata + validate-only
scripts/generate_release_bundle.py 生成干净发布包
scripts/scan_release_bundle.py 扫描发布包是否含敏感/脏文件
scripts/write_release_metadata.py 写 metadata 和 .sha256
scripts/publish_release_bundle.py Python 版上传 + preflight + 可选 apply
scripts/check_text_integrity.py 检查 UTF-8 / 控制字符 / Markdown fence
```
服务器侧脚本:
```text
deploy/preflight-release-host.sh 服务器发布前检查
deploy/apply-release-bundle.sh 校验、备份、应用发布包、重建、验证
deploy/sync_webapp_to_container.sh 同步 /app 与 /app-v2 静态资源到容器
```
## 6. 当前验证状态
本地 release gate 已通过:
- Python 编译检查;
- deploy shell `bash -n`
- deploy excludes 检查;
- 文本完整性检查;
- `/app-v2 build:safe`
- app-v2 敏感扫描;
- 宝塔专项测试 60 个;
- 发布辅助脚本测试 5 个。
合计 Python 测试:
```text
65 passed
```
发布包验证:
- tarball scan 通过;
- `apply-release-bundle.sh --validate-only` 通过;
- metadata / `.sha256` / tar 实际 SHA 一致;
- 不包含 `tmp/pytest-publish-release`
- 不包含高置信真实敏感信息。
## 7. 发布方式
### 7.1 当前推荐:发布包方式
发布包:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
只上传并执行 preflight
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs
exus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
确认 preflight 没问题后正式发布:
```cmd
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs
exus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^
--apply
```
### 7.2 Gitea 发布方向
已有远端 Gitea 仓库:
```text
http://66.154.115.8:3000/admin/Nexus.git
```
如果要用局域网机器 `192.168.124.219` 做新的代码托管平台,建议安装:
```text
Gitea + Act Runner
```
含义:
- Gitea:代码仓库、分支、Web 管理;
- Act Runner:执行 Gitea Actions / CI/CD / 自动发布。
推荐最终流程:
```text
开发机 / Codex
-> git push 到 Gitea
-> Gitea Actions 触发
-> Act Runner 跑 gate / 打包
-> SSH 到正式服务器
-> apply-release-bundle.sh 发布
-> 验证 /health /app/ /app-v2/
```
第一次建议先做手动触发或 release 分支触发,不要一 push 就直接发布生产。
## 8. 测试部署机说明
局域网测试部署机:
```text
192.168.124.219
```
已知用途:
- Nexus 测试部署机;
- Web 入口曾记录为:`http://192.168.124.219:18600/app/`
- 本地 Git 工作树:`/opt/nexus-dev-current`
- 曾用于 pytest、代码上下文索引、Zvec 部署等。
当前记录显示:它有本地 Git 仓库/工作树,但没有明确证据表明已安装 Gitea 服务。
## 9. 安全注意事项
1. 不要提交 `.env`、私钥、真实 token、宝塔 key、数据库密码;
2. 发布包已排除 `.env``docker/.env.prod``web/data`、venv、node_modules、缓存等;
3. `/app-v2` 不显示全局 API Key
4. 宝塔一键登录必须走后端接口,不在前端生成 tmp token;
5. `script_service` 是管理员远程执行命令功能,属于产品能力,不按普通漏洞处理;
6. Gitea Actions 如要生产发布,必须先配置好 Secrets 和最小权限 SSH Key。
## 10. 重要文档索引
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-index-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-ready-checklist-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-manual-deploy-commands-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-python-publisher-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-publisher-tests-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-app-v2-双后台实施说明-20260707.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-API服务数据库调用链索引报告-20260707.md
```
## 11. 当前未完成事项
- 线上 `https://api.synaglobal.vip` 尚未由 Codex 亲自完成 SSH 发布验证;
- 原因是本线程仍使用 `auto_review` 审批,当前 CC Switch 不支持 `codex-auto-review`
- 用户可手动执行发布命令,或改用 Gitea/Act Runner 发布。
@@ -0,0 +1,297 @@
# Nexus 分支合并清单:`main``release/20260708-axs`
> **编写日期**2026-07-09
> **用途**:合并评审 / Gitea PR 描述 / 冲突解决对照表
> **Gitea**https://axs.kuma1xn.vip/admin/Nexus
---
## 1. 两分支现状
| 分支 | 提交 | 说明 |
|------|------|------|
| **`main`** | `5520434d` | 主项目全量历史(598 提交);含运维侧 6~7 月功能 + **一键登录 bootstrap→SSH 回退** |
| **`release/20260708-axs`** | `74149e0d` | 发布快照(孤儿 squash);含 **宝塔 Session 12h 掉线修复** + **`/app-v2`** + 发布制品链 |
两分支 **无共同祖先**,不能普通 fast-forward,需 `--allow-unrelated-histories` 或按模块 cherry-pick。
**推荐合并方向**`release/20260708-axs``main`(保留 main 历史,吸收 release 独有能力)。
```bash
cd /path/to/Nexus
git fetch https://axs.kuma1xn.vip/admin/Nexus.git main release/20260708-axs
git checkout main
git merge FETCH_HEAD:release/20260708-axs -m "merge: release/20260708-axs 宝塔 Session + app-v2" --allow-unrelated-histories
```
---
## 2. `main` 已有、合并时需保留的功能
### 2.1 宝塔一键登录:bootstrap 失败恢复 SSH 回退(2026-07-08 · 运维侧)
**问题**2026-06-21「自动 bootstrap」后,新机 API 未就绪时 bootstrap 失败直接 `raise`,不再走 SSH `tools.py`,一键登录整链中断。
**行为**
- 未配置 API 时仍先 `source=immediate` bootstrapbest-effort
- 非凭据类错误(`ssh_command_failed``bt_not_installed` 等)→ **继续 SSH 临时登录**
- 仅 `ssh_auth_failed` / `ssh_no_credentials` / `ssh_sudo_required` / `missing_center_ip` 硬中断
**关键代码**
| 文件 | 要点 |
|------|------|
| `server/application/services/btpanel_service.py` | `BOOTSTRAP_LOGIN_HARD_FAIL_ERRORS``_create_login_url_fresh` |
| `server/infrastructure/btpanel/ssh_bootstrap.py` | 检测 `class/common.py`(非空 panel 目录);写 `api.json``makedirs` config |
| `tests/test_btpanel_login_url.py` | `test_create_login_url_falls_back_to_ssh_when_bootstrap_fails` |
**Changelog**`docs/changelog/2026-07-08-btpanel-bootstrap-bt-installed-check.md`
---
### 2.2 宝塔一键登录:基础能力(main 历史,release 亦有部分重叠)
| 功能 | 文件 |
|------|------|
| 未配置时自动 bootstrap + 倒计时 | `useBtPanelLogin.ts``btpanel_service.py`2026-06-21 |
| 临时 token 24h | `BT_TEMP_LOGIN_TTL_SECONDS``btpanel_service._login_url_via_api` |
| 登录 URL 短窗口 Redis 缓存 + 去重 | `login_url_lock.py``create_login_url` 双检锁 |
| 主列表/终端一键登录按钮 | `ServersPage.vue``TerminalPage.vue``ServerTableRowActions.vue` |
| 批量获取 API | `useBtPanelBatchBootstrap.ts` |
| API 端口漂移 SSH 修复 | `_try_login_url_via_api``_repair_ip_whitelist` |
---
### 2.3 Agent 监测与告警
| 功能 | 文件 | Changelog |
|------|------|-----------|
| 告警连续超阈门控(默认 3 次才 Telegram | `server/utils/alert_streak.py``alert_metrics.py``watch_alerts.py` | `2026-07-04-alert-streak-gate.md` |
| Agent CPU 双模式采样 | `web/agent/cpu_metrics.py``server/api/agent.py` | `2026-07-02-agent-cpu-sampling.md` |
| 监测槽双模式 / Pin | `watch_service.py``useWatchPins.ts` | `2026-07-01-agent-watch-dual-mode.md` |
| 离线巡检发报前 SSH 复检心跳 | `offline_daily_report_schedule.py` | `d50120d1` |
| 统计卡:未设路径 / 离线 | `useServerStatsCards.ts` | `2026-07-02-stats-unset-path-offline-card.md` |
---
### 2.4 服务器列表与批量
| 功能 | 文件 |
|------|------|
| 分类筛选栏 / 备注弹窗 / 站点链接 | `ServerCategoryFilterBar.vue``ServerRemarkDialog.vue``serverSiteUrl.ts` |
| 未设路径表与主表对齐 | `ServerUnsetPathPanel.vue` |
| 检测路径:搜 `crmeb` 目录 | `server_batch_common.py` | `2026-07-05-detect-path-crmeb.md` |
| IP/域名定时检测 | `ip_domain_detect_schedule.py``ip_domain_detect_loop.py` | `2026-06-24-ip-domain-detect-schedule.md` |
| nginx 域名探测 | `nginx_domain_detect.py``site_host.py` |
| 跨服务器文件传输增强 | `server_file_transfer_service.py``useServerFileTransfer.ts` |
| 上传目录权限探测 | `files_upload_permissions.py` |
---
### 2.5 后台清理与配置
| 功能 | 文件 |
|------|------|
| 告警日志 / 审计日志定期清理 | `alert_log_purge.py``audit_log_purge.py``alert_log_repo.py` |
| 订阅 IP 刷新主 IP 修复 | `ip_allowlist_refresh.py` | `2026-06-23-subscription-ip-refresh-primary-fix.md` |
---
### 2.6 部署与发布源
| 功能 | 文件 |
|------|------|
| 默认 Gitea`axs.kuma1xn.vip`HTTPS | `deploy/nexus-1panel.sh``nexus-1panel.secrets.sh.example` |
| 全量分批推送(避 Cloudflare 524 | `scripts/git-push-axs-full.sh` |
| 本机 rsync 生产部署 | `deploy/rsync-local-to-server.sh``deploy-production.sh` |
---
## 3. `release/20260708-axs` 独有、合并时必须带入 `main` 的功能
> 详见 release 内 `docs/release/20260708/PR-MERGE-说明-20260709.md`
### 3.1 宝塔 Session 12 小时掉线修复(核心)
**现象**:多窗口一键登录宝塔,约 1~2h 刷新后陆续掉线。
**根因**:部分宝塔 `task.py` 硬编码 `3600` 秒清理 `tmp_login` session,与 Nexus 24h tmp_token 不一致。
**行为**
- SSH bootstrap / 一键登录前检测并 patch `task.py` → 读 `public.get_session_timeout()`,默认 `86400`
- 远端 `flock` + `mktemp` + atomic replace
- `extra_attrs.bt_panel` 记录 `session_cleanup_ttl_ok``session_cleanup_task_state`
- 登录 API 响应带 `diagnostics`TTL 修补状态)
- `bootstrap_lock``wait_timeout_seconds`,避免并发 bootstrap 互踢
**关键文件**
```text
server/application/services/btpanel_service.py # _ensure_session_cleanup_ttl_for_login 等
server/infrastructure/btpanel/ssh_bootstrap.py # ensure_session_cleanup_ttl() 远端脚本
server/infrastructure/btpanel/bootstrap_lock.py
server/api/btpanel.py # diagnostics 字段
tests/test_btpanel_temp_login_ttl.py
tests/test_btpanel_bootstrap_lock.py
tests/test_btpanel_get_config.py
```
---
### 3.2 `/app-v2` 第二后台
| 项 | 说明 |
|----|------|
| 路径 | `/app-v2/`(旧 `/app/` 保留) |
| 技术栈 | React + TS + Vite + shadcn/ui + TanStack Query/Table + Zustand |
| 目录 | `frontend-v2/``web/app-v2/` |
| 构建 | `Dockerfile.prod` 多阶段、`frontend-v2/scripts/scan-app-v2-sensitive.mjs` |
| 路由 | `server/main.py` 挂载静态 |
---
### 3.3 发布制品化工具链
```text
scripts/prepare_release_artifact.py
scripts/generate_release_bundle.py
scripts/scan_release_bundle.py
scripts/write_release_metadata.py
scripts/publish_release_bundle.py
scripts/run_local_release_gate.py
deploy/preflight-release-host.sh
deploy/apply-release-bundle.sh
```
发布包 SHA256release 记录):`46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b`
---
### 3.4 文档与 Skillrelease 独有目录)
```text
docs/release/20260708/ # 宝塔 Session、app-v2、安全巡检、发布记录
.skills/nexus-btpanel-review/
.skills/nexus-security-review/
.skills/nexus-ssh-safety/
```
---
## 4. 合并冲突热点(必须手工合)
### 4.1 `server/application/services/btpanel_service.py`
**两边都要保留**
| 来源 | 保留内容 |
|------|----------|
| **main** | `BOOTSTRAP_LOGIN_HARD_FAIL_ERRORS`bootstrap 失败 → SSH 回退(非凭据类不 raise) |
| **release** | `_ensure_session_cleanup_ttl_for_login``_session_cleanup_ttl_*``diagnostics``BootstrapLockTimeout` |
合并后 `_create_login_url_fresh` 推荐顺序:
1. 未配置 → immediate bootstrap(失败按 main 规则决定是否继续)
2. 已配置 → `_ensure_session_cleanup_ttl_for_login`
3. API 临时登录 → SSH 回退
### 4.2 `server/infrastructure/btpanel/ssh_bootstrap.py`
| 来源 | 保留 |
|------|------|
| **main** | `common.py` 真安装检测;`makedirs` config |
| **release** | `ensure_session_cleanup_ttl()``atomic_write_json``flock``session_cleanup_*` 字段 |
### 4.3 `server/main.py` / `Dockerfile.prod`
- 从 **release** 带入 `/app-v2` 静态挂载与构建阶段
- 保留 **main** 现有中间件与安装向导逻辑
### 4.4 `tests/test_btpanel_login_url.py`
- 合并 release 的 session TTL / lock 测试
- 保留 main 的 `test_create_login_url_falls_back_to_ssh_when_bootstrap_fails`
---
## 5. 合并后验证清单
```bash
# 单元测试(合并冲突解决后)
.venv/bin/pytest tests/test_btpanel_login_url.py tests/test_btpanel_ssh_bootstrap.py \
tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_bootstrap_lock.py -q
# 发布门禁(若合并了 release 脚本)
bash scripts/run_local_release_gate.sh
```
**人工验收**
- [ ] `/app/` 旧后台 200
- [ ] `/app-v2/` 新后台 200(若合并 app-v2
- [ ] 新机一键登录:bootstrap 失败仍能 SSH 打开(main 修复)
- [ ] 已装宝塔一键登录后 **2h 刷新仍在线**release Session 修复)
- [ ] 同一子机连点一键登录不互踢 tokenlogin_url_lock
- [ ] `GET /api/btpanel/servers/{id}/login-url``diagnostics`release
---
## 6. 合并后部署
```bash
# 生产(中心机)
cd /opt/nexus
git remote set-url origin https://axs.kuma1xn.vip/admin/Nexus.git
git fetch origin main && git checkout main && git reset --hard origin/main
sudo env NEXUS_ROOT=/opt/nexus bash deploy/nexus-1panel.sh upgrade --skip-git
# 或 release 包路径
bash deploy/preflight-release-host.sh
bash deploy/apply-release-bundle.sh
```
---
## 7. Gitea PR 摘要(可直接粘贴)
```markdown
## 合并摘要
`release/20260708-axs` 合并入 `main`,统一 SSOT。
### release 带入
- 宝塔 task.py hardcoded 3600 → session_timeout 自动修复(解决 12h 掉线)
- `/app-v2` 第二后台
- 发布制品化 / preflight / apply-release-bundle
- bootstrap_lock、login diagnostics
### main 保留
- 一键登录 bootstrap 失败 → SSH 临时登录回退(新机 API 未就绪)
- ssh_bootstrap 真安装检测(common.py+ config makedirs
- 6~7 月运维功能:告警连续门控、crmeb 路径检测、文件传输、日志清理等
- axs Gitea 默认发布源 + 分批推送脚本
### 冲突重点
`btpanel_service.py``ssh_bootstrap.py` — 两套宝塔逻辑必须同时保留,见本文 §4。
### 验证
pytest 宝塔专项 + `/app` + `/app-v2` + 2h 刷新不掉线
```
---
## 8. 相关文档索引
| 文档 | 路径 |
|------|------|
| release 原 PR 说明 | `docs/release/20260708/PR-MERGE-说明-20260709.md`(在 release 分支) |
| 宝塔掉线方案 | `docs/release/20260708/Nexus-宝塔一键登录长期掉线修复方案与代码改进.md` |
| app-v2 说明 | `docs/release/20260708/Nexus-app-v2-双后台实施说明-20260707.md` |
| main bootstrap 回退 | `docs/changelog/2026-07-08-btpanel-bootstrap-bt-installed-check.md` |
| Gitea 发布说明 | `docs/release/20260708/README-Gitea发布说明-20260708.md` |
@@ -0,0 +1,362 @@
# Nexus PR 合并说明:宝塔 Session 稳定性修复与 /app-v2 双后台
> 目标分支:`release/20260708-axs`
> 本地提交基线:`1933f0af6e647818e716ed194cdb92d7ed7bb47a`
> 编写日期:2026-07-09
> 推荐用途:复制本文件核心内容到 Gitea Pull Request 描述,用于合并评审。
## 1. 合并结论
建议合并。该分支主要解决 Nexus 的宝塔一键登录在 1~2 小时后刷新掉线的问题,同时加入 `/app-v2` 第二后台与发布制品化工具链。
本分支不是单点热修,而是一组围绕“宝塔长期登录稳定性 + 新后台并行上线 + 发布可验证”的完整改进:
- 一键登录前自动检测并修复宝塔 `task.py` 中硬编码 `3600` 秒清理 `tmp_login` session 的问题;
- 对同一台宝塔服务器的一键登录生成过程加锁,避免并发打开多个宝塔时 tmp token / session 互相覆盖;
- 新增 `/app-v2` 第二后台,保持旧后台 `/app/` 可继续使用,支持渐进替换;
- 增强发布包、发布前检查、发布验证和回滚脚本;
- 补齐代码审查、调用链、安全巡检、发布记录等文档。
## 2. 背景问题
用户实际现象:
```text
Nexus 一键登录宝塔成功
同时打开多台/多窗口宝塔
约 12 小时后刷新页面陆续掉线
```
根因链路:
```text
Nexus 一键登录成功
→ 宝塔生成 tmp_login session 文件
→ 宝塔 BT-Task 定时清理 session
→ 部分宝塔 task.py 硬编码超过 3600 秒删除 tmp_login session
→ 12 小时后刷新
→ tmp_login session 已被删除
→ 宝塔判定掉线
```
此外,同时打开多个宝塔时,同一台宝塔如果被重复触发一键登录,可能出现登录 URL / tmp token 生成冲突,因此本分支加入了分布式锁与远端 bootstrap 锁。
## 3. 本次核心改动
### 3.1 宝塔一键登录长期不掉线修复
关键能力:
- 登录前自动检测宝塔 `task.py` 是否存在 hardcoded `3600` 清理逻辑;
- 自动修复为读取 `public.get_session_timeout()`,无法读取时使用默认 `86400`
- 对新增宝塔也执行兜底检测/修复;
- 一键登录前再次兜底检测/修复,避免漏修服务器;
- 修复过程使用 SSH bootstrap,包含远端 `flock``mktemp`、atomic replace,降低并发和半写入风险;
- 同一宝塔登录 URL 生成加 Redis 分布式锁,防止同一服务器被并发打开时 token/session 互相覆盖。
重点文件:
```text
server/application/services/btpanel_service.py
server/infrastructure/btpanel/login_url_lock.py
server/infrastructure/btpanel/bootstrap_lock.py
server/infrastructure/btpanel/ssh_bootstrap.py
server/api/btpanel.py
```
相关测试:
```text
tests/test_btpanel_bootstrap_lock.py
tests/test_btpanel_get_config.py
tests/test_btpanel_login_url.py
tests/test_btpanel_login_url_route.py
tests/test_btpanel_ssh_bootstrap.py
tests/test_btpanel_temp_login_ttl.py
```
### 3.2 `/app-v2` 第二后台
新增第二后台,路径为:
```text
/app-v2/
```
设计目标:
- 不替换旧后台 `/app/`,双后台并行;
- 保留原功能入口,逐步迁移;
- 更现代、更快、更适合后续 UI 重构;
- 全局 API Key 等敏感值不直接展示真实值。
技术栈:
```text
React + TypeScript + Vite
shadcn/ui + Tailwind
TanStack Query
TanStack Table
Zustand
```
重点文件:
```text
frontend-v2/
web/app-v2/
frontend-v2/scripts/scan-app-v2-sensitive.mjs
server/main.py
Dockerfile
Dockerfile.prod
```
### 3.3 发布制品化与验证工具链
本分支加入/增强了发布包生成、扫描、元数据、发布前检查和发布辅助脚本:
```text
scripts/prepare_release_artifact.py
scripts/generate_release_bundle.py
scripts/scan_release_bundle.py
scripts/write_release_metadata.py
scripts/publish_release_bundle.py
scripts/check_text_integrity.py
scripts/generate_release_diff.py
scripts/run_local_release_gate.py
```
服务器侧脚本:
```text
deploy/preflight-release-host.sh
deploy/apply-release-bundle.sh
deploy/sync_webapp_to_container.sh
```
发布包:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
## 4. 验证结果
本地 release gate 已通过:
```text
Python py_compile
bash -n deploy shell
部署排除规则检查
文本完整性检查
/app-v2 build:safe
app-v2 sensitive scan
宝塔专项 pytest
发布辅助脚本测试
```
测试结果:
```text
65 passed
```
发布包验证:
```text
size_mb=27.02
entries=2409
OK: release bundle looks clean
apply-release-bundle.sh --validate-only passed
metadata / .sha256 / tar 实际 SHA 一致
```
重点人工验证建议:
- `/app/` 旧后台可访问;
- `/app-v2/` 新后台可访问;
- 宝塔一键登录可打开;
- 同一台宝塔连续/并发触发一键登录不会互相覆盖;
- 新增宝塔在一键登录前会自动检测/修复 hardcoded `3600`
- 典型宝塔登录 2 小时后刷新仍保持登录状态。
## 5. 合并风险与边界
### 5.1 主要风险
1. **宝塔远端 patch 风险**
修复逻辑会通过 SSH 修改目标宝塔服务器上的 `task.py`。本项目定位中,管理员远程执行命令属于产品能力,因此这不是普通越权漏洞,但合并后需要继续保持审计日志与失败可见性。
2. **不同宝塔版本兼容性**
部分宝塔版本 `task.py` 内容可能不同。当前逻辑已经使用检测/备份/原子替换/失败回退策略,但建议继续抽检更多系统版本。
3. **双后台静态资源体积**
分支包含旧后台和新后台静态资源,仓库和发布包会变大。当前发布包约 27MB,可接受。
4. **远端批量修复节奏**
线上 420 台里曾识别出 148 台存在 hardcoded_3600。合并后建议继续采用分批检测/修复,不建议一次性对全部服务器无观察窗口地全量变更。
### 5.2 不属于本次合并要解决的问题
- 不在本次合并中重构所有后台 UI
- 不强制删除旧后台 `/app/`
- 不改变 `script_service` 管理员远程执行命令的产品定位;
- 不依赖账号密码替代宝塔 API 登录;账号密码一键登录可作为后续增强方案单独设计。
## 6. 推荐合并方式
### 6.1 推荐路径
推荐在 Gitea 上创建 Pull Request
```text
source: release/20260708-axs
target: main
```
PR 地址:
```text
https://axs.kuma1xn.vip/admin/Nexus/pulls/new/release/20260708-axs
```
建议标题:
```text
release: 宝塔 Session 长期登录修复与 /app-v2 双后台
```
建议合并策略:
```text
Create merge commit
```
理由:本分支是发布型分支,包含后端修复、前端新增、发布脚本和文档。保留 merge commit 更方便后续定位整次发布。
### 6.2 合并前检查
合并前建议在 Gitea 页面确认:
- 目标分支是 `main`
- source 分支是 `release/20260708-axs`
- 文件变更中包含 `docs/release/20260708/` 说明;
- 没有 `.venv``node_modules``.env``.pem` 等运行环境文件;
- PR 描述中粘贴本说明的第 1~8 节。
## 7. 合并后部署方式
### 7.1 仅上传 + preflight
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
### 7.2 正式 apply
preflight 无异常后再加 `--apply`
```cmd
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^
--apply
```
### 7.3 部署后验证
```cmd
.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip
```
期望:
```text
OK: health
OK: /app
OK: /app-v2
OK: online release verification passed
```
## 8. 回滚策略
如果合并后未部署:
```text
在 Gitea revert merge commit 即可。
```
如果已经部署:
1. 使用 `deploy/apply-release-bundle.sh` 生成的备份目录回滚;
2. 恢复上一版 `/opt/nexus` 文件;
3. 重启 Nexus 服务/容器;
4. 验证 `/health``/app/`
5. 如果问题只影响 `/app-v2/`,可临时隐藏 `/app-v2/` 路由或静态资源,不影响旧后台 `/app/`
宝塔远端 `task.py` 修复属于向更长 session timeout 的兼容性修复。若个别宝塔异常,优先按单台服务器恢复宝塔备份文件,不建议全局回滚。
## 9. 文档索引
本分支已包含详细文档,集中在:
```text
docs/release/20260708/
```
建议优先阅读:
```text
docs/release/20260708/Nexus-项目说明文档-20260708.md
docs/release/20260708/Nexus-release-index-20260708.md
docs/release/20260708/Nexus-release-ready-checklist-20260708.md
docs/release/20260708/Nexus-宝塔一键登录长期掉线修复方案与代码改进.md
docs/release/20260708/Nexus-app-v2-双后台实施说明-20260707.md
docs/release/20260708/Nexus-manual-deploy-commands-20260708.md
```
## 10. 可直接复制到 Gitea PR 的摘要
```markdown
## 合并摘要
本 PR 修复 Nexus 宝塔一键登录 1~2 小时后刷新掉线的问题,并新增 /app-v2 第二后台与发布制品化工具链。
### 核心改动
- 一键登录前自动检测/修复宝塔 task.py hardcoded 3600 清理 tmp_login session 的问题;
- 基于 public.get_session_timeout() 或默认 86400 延长 tmp_login session 有效期;
- 新增 Redis 分布式锁,避免同一宝塔并发生成登录 URL/token 互相覆盖;
- SSH bootstrap 使用 flock / mktemp / atomic replace 降低远端 patch 风险;
- 新增 /app-v2 第二后台,旧后台 /app/ 保留;
- 增强发布包生成、扫描、preflight、validate-only、apply 与回滚脚本;
- 补齐发布说明、调用链、安全巡检、宝塔修复记录等文档。
### 验证
- release gate 通过;
- 宝塔专项测试通过;
- 发布辅助脚本测试通过;
- 合计 65 passed
- 发布包扫描通过;
- apply-release-bundle.sh --validate-only 通过;
- 发布包 SHA256: 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b。
### 风险
- 会通过 SSH 对目标宝塔服务器 task.py 做自动修复,已加入检测、备份、锁和原子替换;
- /app-v2 与 /app 双后台并行,不删除旧后台;
- 建议合并后先 preflight,再 apply,并对宝塔一键登录做 2 小时刷新验证。
```
@@ -0,0 +1,25 @@
# Nexus Gitea 鍙戝竷璇存槑锛?026-07-08锛?
鏈洰褰曟敹褰曟湰娆?Nexus 鍙戝竷銆佸鏌ャ€佸疂濉?Session 淇銆?app-v2 鍙屽悗鍙般€佺幆澧冧慨澶嶄笌鍙戝竷楠岃瘉鐩稿叧璇存槑鏂囨。銆?
## 鏈鏍稿績鍐呭
- 瀹濆涓€閿櫥褰曢暱鏈熸帀绾夸慨澶嶏細鐧诲綍鍓嶈嚜鍔ㄦ娴?淇 hardcoded_3600锛岄伩鍏?tmp_login session 琚?1 灏忔椂纭竻銆?
- 鏂板瀹濆鑷姩鍏滃簳锛氭柊澧炴湇鍔″櫒鍜屼竴閿櫥褰曞墠閮戒細杩涜 session 淇妫€娴嬨€?
- /app-v2 绗簩鍚庡彴锛歊eact + TypeScript + Vite + shadcn/ui + Tailwind + TanStack Query/Table + Zustand銆?
- 鍙戝竷鍖呬笌鍙戝竷宸ュ叿閾撅細鏈湴 release gate銆乥undle scan銆乿alidate-only 宸查€氳繃銆?
## 鎺ㄨ崘鍏ュ彛鏂囨。
- Nexus-椤圭洰璇存槑鏂囨。-20260708.md
- Nexus-release-index-20260708.md
- Nexus-release-ready-checklist-20260708.md
- Nexus-manual-deploy-commands-20260708.md
- Nexus-瀹濆涓€閿櫥褰曢暱鏈熸帀绾夸慨澶嶆柟妗堜笌浠g爜鏀硅繘.md
- Nexus-app-v2-鍙屽悗鍙板疄鏂借鏄?20260707.md
## 合并说明入口
- PR-MERGE-说明-20260709.md:用于 Gitea Pull Request 合并评审,可直接复制到 PR 描述。
@@ -0,0 +1,23 @@
{
"schema_version": 1,
"head_sha": "65d8605b6c6b594262a442916388ac7849332945",
"base_ref": "HEAD~1..HEAD",
"branch": "main",
"reviewed_at": "2026-07-01T12:00:00+08:00",
"engine": "manual-gate-catchup",
"verdict": "Ready to merge",
"summary": "补录门禁:reorder 路由顺序正确;快速添加复用 add-by-ipbootstrap skip-reload 已在 6/29 提交;无 P0/P1。",
"files_reviewed": [
"deploy/pre_deploy_check.sh",
"frontend/src/components/TerminalQuickCommandsSettings.vue",
"frontend/src/components/servers/ServerQuickAddDialogs.vue",
"frontend/src/components/terminal/TerminalTabBar.vue",
"frontend/src/composables/servers/useServerQuickAdd.ts",
"frontend/src/pages/ServersPage.vue",
"frontend/src/pages/TerminalPage.vue",
"frontend/src/utils/openServerTerminal.ts",
"server/api/terminal.py",
"tests/test_terminal_quick_commands.py"
],
"findings": []
}

Some files were not shown because too many files have changed in this diff Show More