547 Commits

Author SHA1 Message Date
Nexus Release Bot d3c3e5a74f chore(deploy): switch installer defaults to axs gitea
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
2026-07-09 07:28:47 +08:00
Nexus Release Bot 92834559bc fix(deploy): include app-v2 in legacy deploy scripts
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
2026-07-09 07:20:11 +08:00
Nexus Release Bot 3938170a16 fix(release): complete app-v2 publish chain
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
2026-07-09 06:53:36 +08:00
admin 1c5b45732f Merge PR #1: release 20260708 axs into main
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
Merge release/20260708-axs selected fixes into main via Gitea MCP.
2026-07-09 06:30:09 +08:00
Codex Release Bot 00c9eddd19 chore(mcp): add axs gitea mcp configuration
Nexus CI/CD / test (pull_request) Waiting to run
Nexus CI/CD / e2e (pull_request) Blocked by required conditions
Nexus CI/CD / deploy (pull_request) Blocked by required conditions
2026-07-09 06:25:39 +08:00
Codex Release Bot 6aaf292ad9 release: merge btpanel fixes app-v2 and gitea allowlist 2026-07-09 06:03:37 +08:00
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
r c8efbc2a90 refactor(servers): 行操作移除删除按钮
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
删除改由批量栏「批量删除」执行,降低误删风险。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:35:47 +08:00
r 2cb0b57281 fix(servers): 提高行操作与批量栏文字可读性
tonal/flat 实底按钮、加大字号字重,恢复一键登录全文。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:32:32 +08:00
r 80d6aad54b style(servers): 行操作与批量栏分组图标化 UI
outlined 按钮组 + 批量栏运维/Agent/其他分区展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:29:27 +08:00
r 58843a1ef4 refactor(servers): 精简行操作并收拢批量诊断
一键登录文案、移除监测/站点行按钮,检测路径与诊断移至批量栏。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:23:39 +08:00
r 7add9384c1 feat: 终端路由保持会话 + 服务器列表按创建时间倒序
keep-alive 下终端切换不 dispose SSH;列表/推送/终端默认 newest first。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:11:00 +08:00
r 6720ca3474 feat(servers): 主列表宝塔登录 + 健康检查写心跳
主服务器列表操作列一键登录宝塔;SSH 健康检查写入 Redis 心跳并刷新在线状态。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:05:43 +08:00
r 2e797f86b7 feat(btpanel): 服务器列表增加在线状态列
批量读 Redis 心跳返回 status,前端与主服务器列表样式一致。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:54:04 +08:00
r 5ea5c3a411 fix(btpanel): 一键登录 URL 去掉安全入口路径
tmp_token 登录在 :port/login,非 :port/{admin}/login,后者 nginx 404。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:46:22 +08:00
r 3195787461 fix(btpanel): 非 root SSH bootstrap 自动 sudo 提权
ubuntu 等用户免密 sudo 下宝塔已装但脚本未提权导致 ssh_command_failed;附 ssh_timeout 运维清单。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:40:46 +08:00
r 97c47a120f fix(btpanel): 面板登录 URL 使用宝塔 pyenv 执行 tools.py
系统 python3 缺 psutil 导致 SSH 回退失败;相对 tmp_token 自动拼安全入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:16:33 +08:00
r 028dc2c8b6 fix(btpanel): API base_url 不含安全入口路径
宝塔 API 走 :port/system,安全入口仅用于浏览器登录;修复代理 404。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:10:34 +08:00
r 51b6d0cdff fix(btpanel): API 签名改为 md5(time+token)
与面板 common.py 一致;原双重 md5 导致密钥校验失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:05:39 +08:00
r e5929694c3 fix(btpanel): bootstrap 写入 panel/config/api.json
宝塔读取 config/api.json 而非 data/api.json,错误路径导致白名单与 token 不生效。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:01:40 +08:00
r b768da3ab8 fix(btpanel): bootstrap 误判 exit_code=0 为失败
Python 中 0 or 1 为 1,导致 SSH 成功仍报 ssh_command_failed。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:52:22 +08:00
r 45ca57bf3f fix(ssh): 连接前从 preset_id 解析 SSH 密码
修复宝塔批量引导因仅关联密码预设、行内 password 为空而全部 ssh_command_failed。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:47:22 +08:00
r 14bbfc4ab7 fix(btpanel): 批量获取时跳过已配置 API 的服务器
前端仅提交未配置 ID;后端 batch 任务对已配置记为跳过。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:39:57 +08:00
r 770469c4aa fix(btpanel): 批量勾选提交真实 server id
v-data-table 勾选值为 id 数组,修复误传 undefined 导致 422。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:37:04 +08:00
r b4436143a6 feat(btpanel): 服务器列表批量 SSH 获取 API
支持全部未配置与多选批量引导;未配置 API 时禁用一键登录。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:34:02 +08:00
r 7c99298419 fix(btpanel): 连接配置页始终显示全局设置
未选服务器时不再隐藏整页内容,避免白屏。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:30:03 +08:00
r 461ac81221 fix(btpanel): 接受相对路径 tmp_token 登录链接
宝塔 tools.py 常返回 /login?tmp_token=...,现与 base_url 拼接为完整 URL。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:26:57 +08:00
r 1c4c5af0e4 feat(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
面板登录页改为全量表格与行内一键登录,侧栏首项更名为服务器列表。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:23:26 +08:00
r d00294c86d docs(btpanel): 部署审计与 changelog
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:16:54 +08:00
r 82426b1941 feat(btpanel): 独立宝塔模块与 SSH 自动获取 API
新增侧栏「宝塔面板」菜单、代理 API 与连接配置页;通过 SSH 读写子机
api.json 自动写入凭据,5 分钟后台重试,含审计修复、检测缓存与并发锁。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:13:14 +08:00
r ebab836554 docs: 审计文件清单补全 btpanel client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 08:14:26 +08:00
r ccc5419369 chore: 部署门控 — 今日 changelog/audit、bandit nosec、shell CRLF
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 08:14:05 +08:00
r d8ec4f1af4 fix: 登录白名单校验改用 LOGIN_ALLOWED_IPS 单一数据源
多 worker 下订阅刷新后 UI 已更新但登录仍被拒;另修复刷新失败时前端误清空节点列表。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 08:12:52 +08:00
r 9c9a51263c chore: 本地快照 — 终端 UI、服务器 site_url、后台任务与 Agent 导航
汇总未提交工作区:Servers 列表 site_url 链接、终端页与选机器 UI、SSH 连通性与 Telegram 通知、agent/offline/unset_path 后台调度与 gate_ai_review 门控;更新 AGENTS 与 agent-exploration 规则(仅本机 commit);同步 web/app 构建产物与相关文档。
2026-06-18 06:01:50 +08:00
Nexus Agent 1a79c0fbfb feat(watch): Baota-style server detail drawer
Expose load/process counts, per-core CPU, memory MB breakdown, and
separate CPU/MEM top-5 processes from SSH probes in the detail drawer.
2026-06-14 06:23:45 +08:00
Nexus Agent 86a45612c8 fix(ops): writable Layer3 state dir for cron user
Fall back to ~/.nexus/layer3-health when deploy/var is root-owned.
2026-06-14 04:23:20 +08:00
Nexus Agent 05b0fde197 fix(ops): Layer3 health_monitor state dir and Telegram heredocs
Move fail counters under deploy/var/layer3-health, use docker_cmd with
sudo fallback, and refactor alert messages to avoid quote parse errors.

Also fix watch Sparkline rgba gradients, hide empty probe pagination,
and ship pending slot pause/menu UI polish.
2026-06-14 04:20:55 +08:00
Nexus Agent c91cb1cd9e fix(watch): polish paused slot UI and header action spacing
Replace the large dashed pause panel with dimmed rings and a resume CTA,
and add gap between the monitoring switch and remove button.
2026-06-14 03:34:17 +08:00
Nexus Agent 3faa7a4f1c fix(watch): align four metric rings in one row
Move load ring beside CPU/mem/disk so the slot card matches Baota layout.
2026-06-14 03:25:10 +08:00
Nexus Agent b5419cd2c0 feat(watch): SSH-first probe, drop IO metrics, Baota ring UI
Align live monitoring with Baota: always SSH psutil for CPU/mem/load,
remove network/disk IO rates, and show metrics as circular rings in slots.
2026-06-14 03:19:43 +08:00
Nexus Agent 5c508ed14c fix(watch): eager probe on resume so metrics appear immediately
After pausing clears Redis snapshots, resuming or adding pins now triggers
an immediate probe before list_slots returns, with frontend refresh fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 02:53:02 +08:00
Nexus Agent 825d0297c4 feat(watch): show centered enable switch when slot monitoring is paused
Paused slots now prompt with a large middle switch before showing CPU and memory metrics; header switch only appears while monitoring is on.
2026-06-14 00:38:44 +08:00
Nexus Agent 4bbc47faab feat(watch): polish monitoring history UI with themed charts and empty states
Align watch metrics pages with dashboard fleet trends: theme-aware ECharts,
skeleton loaders, metric color thresholds, and compact I/O layout on slot cards.
2026-06-13 23:29:58 +08:00
Nexus Agent ba7c790f38 feat(watch): show probe history in Beijing time and retire remote browser
Monitoring history charts and tables now format UTC timestamps as Asia/Shanghai.
Remove Playwright worker, global browser panel, and related API routes; open site URLs in a new tab instead. Fix chain test Redis pipeline mock.
2026-06-13 16:32:02 +08:00
Nexus Agent adee25deb6 fix(watch): repair SSH probe script broken by embedded quote in os_release parse
Use chr(34) inside the bash-wrapped python -c probe so monitor slots work again after hardware detail rollout.
2026-06-13 16:10:32 +08:00
Nexus Agent 4acbf04322 feat(watch): show detailed CPU, memory, and disk specs on monitor slots
Expose used vs total capacity, CPU model, swap, OS uptime, and 1/5/15 load from SSH probes and agent heartbeats.
2026-06-13 14:12:09 +08:00
Nexus Agent feb776616f docs: add 2026-06-13 audit/changelog for files new-file fix deploy gate. 2026-06-13 09:09:01 +08:00
Nexus Agent 72042598b0 Fix files page new-file flow and SFTP parent directory creation.
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
Show a clear prompt when no server is selected, wait for the editor workbench before openFile, and mkdir -p parent paths before SFTP writes.
2026-06-13 09:08:17 +08:00
Nexus Agent e95e522d4d docs(audit): gate checklist for Telegram group chat ID detection 2026-06-13 02:05:27 +08:00
Nexus Agent 40cba6a567 fix(settings): detect Telegram group chat IDs via my_chat_member
Extend getUpdates parsing for group joins and update settings UI hints so Chat ID detection works for supergroups, not only private DMs.
2026-06-13 02:04:59 +08:00
Nexus Agent 821cf12ade fix(terminal,alerts): quick-cmd enter on send and offline Telegram channel
Append CR when executing quick commands (fixes trim stripping saved \r) and
allow optional dedicated Telegram bot for server offline alerts only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 01:45:55 +08:00
Nexus Agent deafd46a0e docs(reports): 文件管理移除下载生产验证记录
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 06:52:47 +08:00
Nexus Agent dbb6bc4a55 fix(browser): 门控 — Worker TLS verify 可配置、审计清单与 shell LF
默认校验 Worker HTTPS 证书;审计文件补全变更清单;docker-entrypoint.sh 改为 LF。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 06:50:06 +08:00
Nexus Agent e833b923c8 feat(browser): 远程浏览器 Worker 桥接与顶栏固定悬浮窗
接入 Playwright Worker、会话 WebSocket 与全局浏览器面板(固定于 App Bar 下);
含验证码 stealth、设置项默认音与 URL 安全校验;附带 worker 部署与设计文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 06:49:20 +08:00
Nexus Agent 2b0413c43e feat(files): 移除文件管理下载入口
去掉行内与右键下载;大文件提示改为使用终端。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 06:32:09 +08:00
Nexus Agent a51a5c5341 fix(files): 修复下载流式 SFTP 并收紧列表行高
下载在独立 SFTP 会话中流式传输;文件/终端列表与服务器页一致的紧凑操作与行高。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 05:50:09 +08:00
Nexus Agent 80aaf855a3 feat(terminal): 右侧服务器列表增加搜索
侧栏复用 PickerSearch,按名称/域名筛选并支持搜索历史。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 05:46:34 +08:00
Nexus Agent e42612e75f feat(watch): 监测历史四槽同屏展示趋势
资源趋势 2×2 四图;探针记录默认全部槽位;表头内存/硬盘中文。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 05:44:28 +08:00
Nexus Agent 7a0001d8f4 fix(watch): 恢复指标进度条原三档配色
≥90% 红、70–89% 黄、<70% 绿。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 05:29:54 +08:00
Nexus Agent 3598d38b8e fix(watch): 指标进度条 ≥80% 变红,到期倒计时归零加固
进度条阈值由 70/90 改为 80% 红色;remaining_sec≤0 时立即 refreshPins。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 05:17:38 +08:00
Nexus Agent d5ecb54d6e feat(watch): 监测槽等待探针回传占位 UI
开启监测后无 probe_status 时显示等待文案与加载动画,回传后自动展示指标。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 05:08:03 +08:00
Nexus Agent ac98dc496c fix(watch): 移除监测槽 hover 时 Vuetify 灰遮罩
v-card 关闭 link/ripple 并隐藏 overlay,仅保留外框主题色高亮。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 04:54:24 +08:00
Nexus Agent 49b471eff9 test(e2e): 监测槽时长与指标 UI 生产冒烟
Playwright 验证默认 30 分 chip、内存/硬盘/负载与进度条;附部署验证报告。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 04:40:40 +08:00
Nexus Agent 647ebcb673 feat(watch): 监测槽分钟时长档、指标进度条与负载显示
默认 30 分钟,支持 30分/1时/2时/8时/24时;槽卡展示 CPU/内存/硬盘进度条与 1 分钟负载;API 改用 ttl_minutes。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 04:34:52 +08:00
Nexus Agent 4c38bc05d3 docs(audit): 监测槽 hover 外框高亮 2026-06-12 04:21:28 +08:00
Nexus Agent 907607e30b fix(watch): 监测槽 hover 改为外框高亮
悬停时仅边框变主题色,不再整卡铺灰底。
2026-06-12 04:21:16 +08:00
Nexus Agent bdbfef7110 fix(audit): 目标列显示服务器名称而非 #id
审计列表对 server 类型批量解析 target_name,前端展示为「服务器 {name}」。
2026-06-12 04:16:46 +08:00
Nexus Agent 8092000880 fix(watch): psutil SSH 安装 apt 走 sudo
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
非 root 用户 apt-get 未加 sudo 导致 Permission denied;补充 run_root 与中文错误提示。
2026-06-12 04:09:10 +08:00
Nexus Agent e57d689fb2 docs(audit): 补 test_watch_probe_errors 到 psutil 安装审计 2026-06-12 04:04:59 +08:00
Nexus Agent 75efd506f5 feat(watch): 监测槽 SSH 一键安装 psutil
探针报 psutil missing 时显示安装按钮;POST install-psutil 经 SSH pip/apt 安装并审计。
2026-06-12 04:04:48 +08:00
Nexus Agent 32a1885f0d feat(watch): 监测槽开关、8h/24h、到期暂停与探针修复
支持随时暂停/恢复实时监测并保留槽位;TTL 到期自动暂停不占删槽;修复到期竞态导致空槽与唯一约束冲突;探针 parse_error 与中文错误;移除失败 snackbar。
2026-06-12 03:47:56 +08:00
Nexus Agent 7bb85aac41 feat(servers): 新服务器 Agent 安装/升级引导提示
修正仅有 API Key 误判为已装 Agent;列表与展开详情提示安装/升级并提供一键操作。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 03:16:12 +08:00
Nexus Agent f37047f746 docs(audit): 文件搜索与监测槽点击修复审计
补 Step3/Closure/DoD;选机对话框加载失败改 snackbar 提示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 03:10:15 +08:00
Nexus Agent 6b8965aaf6 fix(watch): 监测槽空槽选机与卡片点击进历史
空槽弹出搜索选机对话框;已填槽整卡可点进监测历史;已在监测的 + 给出提示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 03:07:00 +08:00
Nexus Agent 6140d68875 feat(files): 文件管理页服务器选择支持搜索
2000+ 子机场景下可搜索名称/域名/路径/ID,并加载全量服务器列表。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 03:02:28 +08:00
Nexus Agent b46922fe92 feat(watch): 4 槽实时监测 — 5s 探针、WS、历史页与告警联动
宝塔式自选子机监测:Pin CRUD、全指标 SSH/Redis 探针、探针落库、
/ws/watch 推送、监测历史与进程抽屉;Agent 可选附带 net/disk IO。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 02:56:37 +08:00
Nexus Agent f6e8b29640 fix(ui): 服务器/终端/推送搜索历史互不共享
终端独立 terminal_search 上下文(10条),与 servers_search、push_search 隔离。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 02:03:45 +08:00
Nexus Agent e0133b9098 feat(push,terminal): 推送搜索历史5条与终端中间搜索
推送页独立 combobox 历史(MySQL push_search,上限5);终端空态搜索回中间卡片,右侧栏仅列表。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 01:58:33 +08:00
Nexus Agent 724375c884 fix(push): 移除点击复制服务器名称
名称点击恢复为与卡片/行一致的勾选行为。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 01:51:39 +08:00
Nexus Agent 989b17902b feat(terminal): 右侧服务器列表与搜索历史
终端与会话并行:右侧常驻选服列表,combobox 共用服务器页最近 10 条搜索历史。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 01:47:46 +08:00
Nexus Agent 441aa0b9d2 docs: 部署门控审计(移除浏览器 + 推送 UI)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 01:44:17 +08:00
Nexus Agent 540712500f fix: 移除 iframe 浏览器并优化推送页服务器展示
删除内置浏览器前后端;推送页支持卡片/列表切换、点击名称复制与长名称展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 01:43:51 +08:00
Nexus Agent 17abd62261 feat(servers): 新服务器接入自动 sudo 配置与路径探测
创建/导入/IP 添加成功后后台 onboard 任务;前端注册进度条与 toast。
2026-06-12 00:19:31 +08:00
Nexus Agent 3a4b2ff3eb docs(reports): 未设路径 fleet 探测与推送验证报告 2026-06-11 23:50:11 +08:00
Nexus Agent 2877fd90a4 fix(scripts): batch_detect 不依赖 Redis 批任务结构 2026-06-11 23:48:28 +08:00
Nexus Agent e6abd4ae73 chore(deploy): 补 Gate7 审计与 shell LF 修复 2026-06-11 23:45:44 +08:00
Nexus Agent 29b053b3c0 fix(sync): 未设路径推送回退 /www/wwwroot,UI 判定不变
空或占位 /www/wwwroot 仍算「未设路径」;rsync 推送/预览/验证改用 resolve_push_target_path,并新增批量 detect-path 脚本。
2026-06-11 23:42:58 +08:00
Nexus Agent f11ad35708 chore(scripts): 推送提权验证脚本与温胜夜生产验证报告
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
2026-06-11 23:28:59 +08:00
Nexus Agent 84b388fae2 fix(sync): rsync 推送非 root 自动 sudo 提权
文件推送与文件管理对齐:非 root 默认 auto_sudo 时使用 sudo -n rsync;
新增批量 sudoers 补丁脚本以补全 rsync 白名单。
2026-06-11 23:20:45 +08:00
Nexus Agent 0a5111a959 docs(audit): 浏览器拖动手柄修复审计条目
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 20:20:02 +08:00
Nexus Agent 31846c4212 fix(browser): 修复浮动浏览器最小化条与面板无法拖动
迷你窗口使用独立最小尺寸与拖动手柄;展开态增加左侧拖拽条。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 20:19:49 +08:00
Nexus Agent 2e8daad802 fix(browser): 不可关闭、最小化可拖拽与重启
移除关闭入口;最小化为可拖动浮动条并持久化 minimized_rect;重启保留浏览历史。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 20:15:40 +08:00
Nexus Agent a94d340ec0 docs(audit): 补全全局浮动浏览器 Closure 与文件清单
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 20:11:21 +08:00
Nexus Agent 8356425814 feat: 全局浮动浏览器、记录持久化与角落快捷入口
浮动面板可最小化至右下角、左下角随时展开;标签与浏览记录写入 MySQL,切换菜单保持活动。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 20:10:57 +08:00
Nexus Agent 2a6f526bc9 feat: 内置 Web 浏览器页与服务器站点入口
侧栏浏览器页 iframe 预览 http(s) 站点;服务器列表「站点」由 domain 跳转,禁止嵌入时可新标签打开。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 18:30:44 +08:00
Nexus Agent 996403ad86 docs(audit): 文件上传 www 权限 fixup 审计记录
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 18:24:06 +08:00
Nexus Agent 4e117c04ea feat(files): 上传后自动 chown www:www 与文件 chmod
复用推送的 RSYNC_PUSH 配置,上传成功即 SSH 修正属主/权限;失败时仍保留文件并返回 warning。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 18:23:31 +08:00
Nexus Agent 79105dcec8 fix(files): 改权限后列表不更新及 chown 连带失败
ETag 纳入权限指纹避免 304 旧数据;chmod/chown 分步执行;仅变更属主/属组时才提交 chown。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 18:12:57 +08:00
Nexus Agent d3c0e57a08 fix(servers): 搜索改为手动触发,进页默认全量列表
不再自动恢复 last 搜索;下拉选历史、回车或清空才请求;输入过程不触发过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 17:54:46 +08:00
Nexus Agent af912662de fix: combobox 清空 null 导致服务器列表加载失败
v-combobox 清空时 search 为 null 使 trim() 抛错;422 响应剥离不可序列化的 ctx 避免 500。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 17:52:49 +08:00
Nexus Agent c69e45a571 docs: 服务器搜索历史部署审计与门控记录
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 17:49:43 +08:00
Nexus Agent 130cc840c7 feat: 服务器搜索历史持久化到 MySQL
按管理员将 Servers 页搜索记录存入 admin_ui_preferences,换设备登录仍可恢复;首次自动迁移 localStorage。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 17:46:52 +08:00
Nexus Agent 704764bd7e fix: 订阅白名单多 worker 同步并统一上传上限 500MB
保存 IP 白名单时同步刷新并 Redis 广播 login_* 设置;推送/文件上传与 Nginx 模板对齐为 500MB,避免生产 50m/100m 导致大文件失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 17:39:26 +08:00
Nexus Agent 426bafd169 Audit: list package-lock.json in RDP removal review. 2026-06-10 23:23:28 +08:00
Nexus Agent dc4593f8b5 Remove browser RDP (guacd/Guacamole) feature entirely.
Product decision: drop 3389 remote desktop page, API, guacd sidecar, and related tests after unresolved target-side black screen issues.
2026-06-10 23:23:09 +08:00
Nexus Agent 341b130d9b fix(rdp): guacd 后台读取队列 + 隧道超时与尺寸
独立 guacd reader 避免 WS 竞争丢数据;ping 回显与 sync 已生产验证;前端 90s 超时与 OPEN 后发尺寸。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:54:50 +08:00
Nexus Agent af47b24450 fix(rdp): connect 前挂载画布并修复 RdpPage 布局
Guacamole display 需在 DOM 中才能完成 sync;canvasHost 独立挂载避免 Vue 重绘清除画布。
2026-06-10 21:45:54 +08:00
Nexus Agent aeca2fd7cb fix(rdp): guacd→WebSocket 按完整指令转发,修复黑屏
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
TCP 块转发会截断 img/blob 指令导致 guacamole-common-js 无法渲染;connect 不再经 URL 传密码。
2026-06-10 21:37:10 +08:00
Nexus Agent 960598f504 fix(rdp): 服务端 guacd 握手替代透明 TCP 桥接
guacamole-common-js 不会向 guacd 发送 select/connect;改为 Nexus 用 rdp_hosts 凭据完成握手后再桥接 WebSocket,消除 protocol violation。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:33:01 +08:00
Nexus Agent 6f998048b6 fix(rdp): WS JWT 改路径避免 Guacamole 查询串冲突
Guacamole connect() 追加 ?hostname= 会破坏 ?token= 导致 403。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 21:18:23 +08:00
Nexus Agent 9b9a857720 docs(audit): 补全 rdp_hosts 审计文件清单
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 20:59:09 +08:00
Nexus Agent f127f07ab2 feat(rdp): 3389 页独立管理 RDP 主机
rdp_hosts 表与专用 API,在 3389远程页添加/连接,不再依赖子机列表。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 20:58:46 +08:00
Nexus Agent dabfc128b3 feat: 浏览器 RDP、推送提示音与子机指标等多项功能
- 3389远程页 + guacd 侧车 WebSocket 桥(无 RDP 会话审计)
- 文件推送完成独立提示音,至少一台成功即播放
- 子机列表展开 CPU/内存/磁盘 sparkline 与指标采样落库
- 终端全量服务器列表、连接中状态;告警 Telegram 公网 IP
- SSH 密钥凭据 UI 简化;子机搜索未设路径筛选

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 01:37:32 +08:00
Nexus Agent dd870859b5 docs(changelog): 记录服务器名称列 chip 副标题回退
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 20:45:10 +08:00
Nexus Agent 700b3b4301 Revert "feat(servers): 名称列地址与资源 chip 副标题"
This reverts commit cc34b9c3cc.
2026-06-09 20:44:39 +08:00
Nexus Agent cc34b9c3cc feat(servers): 名称列地址与资源 chip 副标题
拆分 ServerListNameCell/AddressRow/MetricChips,列表展开前可见分类、CPU/内存/磁盘与心跳。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 20:41:34 +08:00
Nexus Agent 53b700f929 fix(dashboard): 移除舰队资源趋势 CSV 导出
产品决定不在趋势区提供 CSV 下载,保留全屏内明细表。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 18:01:45 +08:00
Nexus Agent fc4b46566e docs(audit): 子机离线告警 8 步审计与部署验证
补全 7209f53 门控审计报告与生产验证记录,更新离线统计审计 DoD。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 17:34:29 +08:00
Nexus Agent 7209f53af9 feat: 子机离线 Telegram 告警与仪表盘统计/趋势增强
离线边沿检测推送一次 Telegram(设置开关 notify_alert_offline);对齐仪表盘与服务器页统计卡、舰队趋势交互与告警叠加。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 17:16:48 +08:00
Nexus Agent 283041fe2f docs(audit): gate7 cover test_servers_dashboard 2026-06-09 09:50:51 +08:00
Nexus Agent 85b0c3a980 fix(servers): accurate offline count and status filter
Count online/offline per monitored server via Redis; filter list by UI status; expose offline_list for main table scope.
2026-06-09 09:50:37 +08:00
Nexus Agent cbbcdae66d feat(servers): interactive stat cards and unset_path count
Click offline/online to filter, unset-path to scroll to panel; add unset_path to /servers/stats; fix Docker deploy frontend sync.
2026-06-09 09:42:51 +08:00
Nexus Agent c5e54da8de docs(audit): list all files for agent diagnose UI gate 7 2026-06-09 09:27:04 +08:00
Nexus Agent a82d69c3e5 feat(servers): add Agent diagnose button and API endpoint
Expose central heartbeat + SSH probe diagnostics in the servers UI via POST /api/servers/{id}/agent-diagnose, sharing logic with the CLI script.
2026-06-09 09:26:42 +08:00
Nexus Agent fc837bfd43 feat(ops): add batch Agent remote diagnose script
SSH from central creds to inspect systemctl, redacted config, 401/heartbeat logs, and /health on sub-servers.
2026-06-09 09:19:40 +08:00
Nexus Agent 7a9dc3d3fc fix(credentials): default database credential host to 127.0.0.1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 09:11:34 +08:00
Nexus Agent 1fefa4b6cb feat(credentials): validate RSA/OPENSSH PEM and auto-derive public key
Accepts keys like nz-admin.pem (BEGIN RSA PRIVATE KEY); rejects invalid PEM
before encrypting. Public key field optional on create/update.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 09:05:06 +08:00
Nexus Agent 6a6f3f7614 docs: audit cover web/app build output
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
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 09:00:29 +08:00
Nexus Agent fe8c11b706 fix(credentials): load SSH key presets via getList and validate save form
The ssh-key-presets API returns a plain array; fetchPagePerPage left the
list empty so new keys appeared not to save.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 09:00:00 +08:00
Nexus Agent b287707359 docs: record HTTP detail zh production deploy at 64c8bd7
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 08:56:56 +08:00
Nexus Agent 64c8bd74b8 docs: audit and deploy verification for API detail zh rollout
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 08:54:27 +08:00
Nexus Agent 1c0d7e9eb6 fix(api): localize service-layer ValueError messages to Chinese
So str(exc) passthrough on scripts, batch, sync, and schedule routes
returns Chinese without relying only on the HTTP detail translation layer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 08:53:18 +08:00
Nexus Agent a842af2405 fix(auth): use Chinese fallbacks and map reason codes to user messages
Stop exposing raw reason codes like admin_not_found in TOTP setup errors;
auth routes and JWT guard now use Chinese literals with auth_failure_detail.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 08:44:51 +08:00
Nexus Agent 81004cf0a5 docs: note HTTP detail zh layer in handoff TOP 10
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 08:42:48 +08:00
Nexus Agent 3ba78cd10a feat(api): translate HTTPException detail to Chinese for admin routes
Add a global detail translator for common 4xx messages (Server not found,
auth headers, settings not found, etc.) while keeping /api/agent/* in English.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 08:42:43 +08:00
Nexus Agent 213aad97a4 docs: push category fix verification and refresh handoff to c366ed5
Record production evidence (101 机器人 vs old 5) and update AI handoff
with current deploy state and TOP 10 priorities.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 08:36:14 +08:00
Nexus Agent c366ed5adc fix(push): load full server list for accurate category selection
Push page previously fetched only 200 servers by ID order, so large categories
like 机器人 showed a handful of machines. Also decouple unset-path panel from
the main category chip filter and align category labels with the servers page.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 08:12:50 +08:00
Nexus Agent 664efb9ed5 feat(dashboard): add 24h fleet CPU/mem/disk trends with v-sparkline.
Sample fleet averages every heartbeat flush into fleet_metric_samples;
expose GET /api/servers/fleet-metrics and Dashboard sparkline card (B-02).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 07:24:57 +08:00
Nexus Agent adcd8010a5 docs: record production deploy at 863e49e after full rollout.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 07:14:32 +08:00
Nexus Agent 863e49e518 feat(ops): prod timezone probe, web/app container sync, and D-01 agent upgrade.
Unify upgrade-agent with install.sh via agent_deploy; auto-sync Git web/app after
Docker upgrade to prevent vite hash drift; extend prod_probe for Beijing TZ checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 07:12:38 +08:00
Nexus Agent 56139a42fe docs(audit): cover timezone follow-up files for gate 7 review.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 05:51:56 +08:00
Nexus Agent e3efbd4552 feat(timezone): operator schedules and filters use Beijing wall clock.
Cron matching, audit/alert date filters, and schedule UI align on Asia/Shanghai
while JWT/TOTP/heartbeat stay UTC; follow-ups add WS alert timestamps, fixed
install timezone copy, and 422 on invalid date filters.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 05:50:32 +08:00
Nexus Agent 4d82ed4031 build(frontend): rebuild web/app assets and prune stale hashes.
Align index.html with a complete Vite output set and remove 116 orphaned
chunks so git clone loads the SPA correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 03:33:23 +08:00
Nexus Agent b62f1039db feat(ops): add production probe with dual-path health checks.
SSH origin checks cover auth contracts and once-schedule create/delete
when external HTTPS is blocked; includes install script for probe credentials.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 03:28:17 +08:00
Nexus Agent 9f78207aa2 docs: add agent diff guidance and close deploy verification.
Record .cursorrules rules to avoid full-repo diffs over web/app/assets,
and document production health checks for the alerts/schedule fix batch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 02:52:32 +08:00
Nexus Agent 3864d120fb fix: alerts page size and schedule once fire_at parsing.
Wire alert history items-per-page to the API and parse fire_at ISO strings to datetime so single-shot push schedules save instead of 500.
2026-06-09 02:47:36 +08:00
Nexus Agent 0f6f91e61a fix(api): align alerts/commands contracts and ship full-site test suite.
Restore alert history filters and stats fields, paginate command/session logs for the SPA, show script labels on schedules, and land integration/E2E coverage with rebuilt web assets.
2026-06-09 02:13:13 +08:00
Nexus Agent 091fb97291 Push async submit, Chinese 422 field labels, hide server CSV export.
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
Return sync/files immediately with background runner; show loc_zh in validation errors; remove servers toolbar CSV export button.
2026-06-08 23:28:26 +08:00
Nexus Agent abeab9d3e4 Fix recovery Telegram dedup and unify Beijing time display.
Store alert metrics without per-value Redis members to stop duplicate recovery pushes; format all operator-facing timestamps in Asia/Shanghai across Web UI and Telegram.
2026-06-08 23:18:56 +08:00
Nexus Agent 972456a246 feat(agent): 401 停心跳、配置重载后重启与升级双文件下发
提取 heartbeat_policy;install/upgrade 同步 heartbeat_policy.py;config reload 恢复心跳协程。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 23:06:09 +08:00
Nexus Agent 69068e2e39 feat(ops): server_batch 回收、sync_logs 清理与 Agent 安装跳过
部署对齐三项后端能力:启动/周期收尾僵尸批量任务、30 天推送日志 purge、已安装且版本不低于主站时跳过批量安装 Agent。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 15:23:03 +08:00
Nexus Agent a401ea5a4d fix(servers): server_ids 批量上限 2000 并修复列表 422 中文
全选 101 台批量操作不再因 max_length=50 被拒;Pydantic 列表 too_long 错误显示完整中文。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 15:08:03 +08:00
Nexus Agent 632891e6e5 fix(settings): 表单校验与 422 中文错误信息
v-form 保存前 validate;validation_errors_zh 翻译 Pydantic msg;value 数字 coerce。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 14:52:59 +08:00
Nexus Agent ff143c286d fix(settings): 保存设置时将数值字段转为字符串避免 422
SettingUpdatePayload.value 要求 str,连接池与告警阈值为 number 输入。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 14:50:10 +08:00
Nexus Agent 85ecd1f54f fix(servers): 未设路径区表格绑定 sort-by 启用服务端排序
未设路径面板缺 v-model:sort-by,列头点击不触发 API;与主表共享 sortBy。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 14:36:21 +08:00
Nexus Agent 05c7aeb702 fix(servers): 未设路径批量检测对话框选中数
检测路径与批量改分类确认框按当前区块显示并提交正确 server_ids。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 14:09:50 +08:00
Nexus Agent d0526c06b7 fix(servers): 展开详情对比度与同步日志仅 1 条
展开区灰底可读;行内详情只拉最近一条同步记录,去掉「最近 N 条」文案。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:45:13 +08:00
Nexus Agent cb49fd9d6d docs(audit): gate7 补资源恢复开关联动与仪表盘去列表
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:23:04 +08:00
Nexus Agent fe8b24ca0b fix(notify): 资源恢复 Telegram 联动 CPU/内存/磁盘开关
关闭分项告警后不再推送对应恢复消息;仪表盘移除服务器列表并更新设置页说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:22:38 +08:00
Nexus Agent 6612c76dc6 docs(audit): gate7 补 dashboard 修复 index.html
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:13:43 +08:00
Nexus Agent 86bc737f4c fix(dashboard): 修复服务器列表不显示
清除 keep-alive 下 loading 卡死;改用 v-card-text 与表格内置 loading,并加 30s 自动刷新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:13:34 +08:00
Nexus Agent 27da842a20 fix: Telegram 开关多 worker 同步与服务器行内详情展开
关闭 notify_* 后通过 Redis 广播同步各 worker 内存设置;修复 expanded id 类型不匹配导致点击名称无法展开详情。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:05:22 +08:00
Nexus Agent a5b1d34cec docs(audit): gate7 补 web/app/index.html 覆盖
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 12:55:20 +08:00
Nexus Agent 1e83bb34bd feat(servers): 未设路径区批量管理与主表齐平
独立勾选与全选筛选结果,共用 ServerBatchActionBar;列与批量操作对齐主列表。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 12:55:02 +08:00
Nexus Agent 8c86b50d09 feat(servers): 未设路径区块与行内详情;命令栏恢复单色
服务器页拆分未设路径列表、行内展开详情;API target_path_unset 可选过滤。
命令栏取消 Shell 语法彩色,ANSI 仅保留 xterm SSH 输出区。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 12:48:45 +08:00
Nexus Agent 8312a6cf81 fix(terminal): WebSSH xterm SSH 交互区 ANSI 彩色输出
PTY 环境变量注入 TERM=xterm-256color,DATA 帧 base64 保 ESC;前端解码写入 xterm;命令栏 Shell 高亮 CSS 与黑屏分离。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 12:35:27 +08:00
Nexus Agent 1d1fb6285b fix(terminal): 快捷命令栏「管理」改为「断开」
底部快捷栏提供断开会话,快捷命令配置仍走系统设置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 12:14:47 +08:00
Nexus Agent 835ef2c8ea fix(terminal): WebSSH 固定黑底白字 ANSI 终端配色
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
不再跟随 Vuetify 浅色主题,xterm 始终使用经典黑底白字与 16 色输出。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 12:09:00 +08:00
Nexus Agent 09d4613e46 fix(scripts): script_executions.results 扩 MEDIUMTEXT 并截断落库
366 台批量执行 results JSON 超 TEXT 64KB 导致 flush 失败、状态卡在 running。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 11:57:20 +08:00
Nexus Agent 22d5d21c1f fix(schedules): 单次显示「当天」、收窄重复方式下拉
单次执行不再出现「重复方式」;「每天」改为「当天」;循环模式下拉宽度收窄。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 11:49:20 +08:00
Nexus Agent 08a0157c95 feat: 调度表单重构、登录门控、批量任务与页面缓存对齐
调度页执行周期可视化/单次执行/分类选机/推送源对齐;登录 IP 门控与无缝导航;
服务器批量后台任务、执行记录、凭据合并、各页激活刷新与错误提示修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 11:17:21 +08:00
Nexus Agent b8425cc059 fix(servers): 状态/心跳/Agent 列 overlay 后排序
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:38:28 +08:00
Nexus Agent b220a5a762 fix(terminal): 移除命令栏冗余按钮,断开遮罩仅覆盖 SSH 区
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:30:31 +08:00
Nexus Agent b011b60f9c feat(ui): 服务器/脚本/重试队列页面自动刷新
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:27:46 +08:00
Nexus Agent 442e005d27 docs(audit): 终端修复审计补 index.html
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:24:44 +08:00
Nexus Agent 908ae938b5 fix(terminal): 深链 server_id 下命令输入框可用
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:24:32 +08:00
Nexus Agent b12fb4770b docs(audit): Gate 7 vite assets 跳过门控审计
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:21:31 +08:00
Nexus Agent 6150a39889 fix(deploy): Gate 7 跳过 web/app/assets 哈希产物
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:21:20 +08:00
Nexus Agent 06af90d35e feat(scripts): 执行详情服务器名与顶栏居中提示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:20:37 +08:00
Nexus Agent 9723e0f528 docs(audit): Gate 7 diff 修复门控审计
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:09:59 +08:00
Nexus Agent 118ec9f6b0 fix(deploy): Gate 7 仅对比 HEAD~1..HEAD 并跳过 gate_log
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:09:46 +08:00
Nexus Agent 3d76c4a447 docs(audit): 补齐脚本看板部署门控所需审计段落
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:08:28 +08:00
Nexus Agent 0b2d7ac50d feat(scripts): 异步执行、全局进度看板与完成提示音设置
脚本 exec 立即返回 running 并在后台跑批次;/ws/alerts 推送 script_progress/complete;
新增 ScriptRunsPage、Telegram 汇总通知与可配置的浏览器完成提示音。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:07:02 +08:00
Nexus Agent 3f995a74bb docs: 更新 AI handoff 至 df95a24
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 05:51:57 +08:00
Nexus Agent df95a2429f feat(frontend): 全选筛选结果与脚本失败分组复制
服务器列表支持跨页全选当前筛选/分类本组;脚本执行详情按错误类型分组(去动态 IP)并复制失败清单,不做 CSV 导出。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 05:51:11 +08:00
Nexus Agent 5f63fb0a3f fix(scripts): 执行进度按成功台数计算,失败项快速定位与面板清理
progress 改为 success/total(失败不计入分子);脚本库执行详情中文化、失败筛选与终态任务移出批量状态面板。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 05:11:39 +08:00
Nexus Agent c435413563 feat(servers): 分类筛选/批量改分类、分页中文与列排序,脚本库按分类选机
支持 300+ 台服务器按分类浏览与批量管理,修复分页「全部」与排序;门控 Gate3 强制本地 API。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 04:30:24 +08:00
Nexus Agent c8b0663508 feat: 批量IP添加、脚本库历史、推送权限记录与脚本SSH执行修复
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
批量添加替代CSV并支持去重;脚本库页内嵌执行历史;推送历史记录 rsync 权限策略;
脚本执行不再依赖 Agent 在线;服务器同步日志与相关单测补齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 03:15:40 +08:00
Nexus Agent fb6a4be797 feat(commands): 推送日志第三视图与 CSV 导入模板
按对齐计划 Phase1:Commands 页增加推送日志筛选视图;补全 servers_import_template.csv;扩写功能指南 §12.6/12.8/12.10 与 acceptance_catalog。
2026-06-08 02:45:17 +08:00
Nexus Agent dfb0ea2d8f fix(frontend): 脚本库与推送调度对齐设计文档
修复 ScriptsPage 执行契约(script_id/command、历史过滤、长任务/凭据)及 SchedulesPage 缺失能力(target_path、next_run、push/script、cron/once、sync_mode);后端补迁移与 schedule_runner 传参。
2026-06-08 02:39:18 +08:00
Nexus Agent f12a0b6a36 fix(sync): rsync 推送后设置远程属主 www 与权限 755
推送默认 --chown=www:www --chmod=D755,F755,避免文件落盘为 root。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 02:23:43 +08:00
Nexus Agent 5ed3c28491 fix(sync): 推送异常时落库 failed,修复订正时区与卡住阈值
rsync 抛错时 sync_log 不再永久 running;reconcile 兼容 naive UTC。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 02:14:33 +08:00
Nexus Agent 77cd9a5205 fix(docker): 生产镜像安装 rsync 修复文件推送失败
容器内缺少 rsync/openssh-client/sshpass,_rsync_push 触发 FileNotFoundError。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 02:10:06 +08:00
Nexus Agent 814262bd83 feat(push): 暂存文件支持批量删除
推送页暂存列表增加多选、全选与批量删除确认,复用 local-file-ops 逐条删除。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 02:05:33 +08:00
Nexus Agent 2b016f91af fix(push): 普通文件上传不限制后缀
upload-files 接受任意后缀含 .zip;拖拽/多选统一原样暂存,解压改走「ZIP 解压上传」按钮。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 01:19:20 +08:00
Nexus Agent 217c5d9935 feat(push): 源路径验证、普通文件上传与暂存文件管理
补齐 Round4 H5/H6:验证 Nexus 主机源路径、失败批量重试;新增 upload-files 与普通文件多选上传;上传后可浏览/预览/重命名/删除暂存目录内容。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 01:14:07 +08:00
Nexus Agent a225b655d9 feat(frontend): 恢复服务器批量检测路径并修复推送目标路径
补回 SPA 迁移丢失的 detect-path 确认框与结果弹窗,推送页留空走各机 target_path,文件页默认 50 条/页。
2026-06-08 00:56:35 +08:00
Nexus Agent bdb3b3bc34 docs(deploy): 生产对齐部署验证报告与 changelog 补全
记录 e5a3f25 部署、前端镜像重建与 Agent 1/1 升级结果。
2026-06-07 23:22:21 +08:00
Nexus Agent e5a3f2524e fix(auth): change_password 改密路径 audit_repo 定义顺序
部署前补齐门控审计与 Layer3 演练终验记录,消除 ruff F821。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 23:11:28 +08:00
Nexus Agent 9337f37da8 fix(files,deploy): 文件上传字段名与 Layer3 巡检脚本
后端同时接受 file/files 并支持多文件 SFTP 上传;修复 health_monitor.sh 引号语法使 Layer 3 cron 可运行。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 23:00:01 +08:00
Nexus Agent 7ea94a60ab fix(auth,servers): 修复 refresh 自动登出与服务器状态未知
API 返回 status 字段供前端显示在线状态;token_version 为 NULL 时 refresh cookie 含 :None 导致续期 401。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 22:53:29 +08:00
Nexus Agent 05006cb5df feat(auth): refresh token 先 Redis 后 MySQL 双写
登录 30 天会话 hash 持久化到 admin_refresh_tokens;启动从 MySQL 回填 Redis,
Redis 重启后会话仍可 refresh。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 21:56:56 +08:00
Nexus Agent 69087988b1 fix(auth): 反代后登录白名单读取真实客户端 IP
Docker/1Panel 反代时 request.client.host 为 172.18.0.1;可信内网跳读取 X-Real-IP/X-Forwarded-For。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 21:50:39 +08:00
Nexus Agent a14fbb3df3 fix(ssh): 快速添加跳过主机密钥校验并中文化错误
凭据轮询在 SSH_STRICT 默认 true 时因 HostKeyNotVerifiable 在认证前失败;
探测始终 known_hosts=None,默认 strict 改为 false,错误信息统一中文。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 21:31:38 +08:00
Nexus Agent c50f65e8ea docs: Layer 3 巡检 Telegram 用户终验确认
记录用户已收到引号修复与中文详细通知消息,闭环终验通过。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 21:09:14 +08:00
Nexus Agent ea2507dbc0 feat(telegram): 全部通知改为中文详细说明
统一告警/恢复/系统/Layer3/测试消息格式,含来源、影响与建议操作。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 21:01:47 +08:00
Nexus Agent f60a5ea5bc fix(deploy): health_monitor 剥离 .env 引号以修复 Telegram 404
容器 .env 带引号的 Token 导致 sendMessage URL 无效;增加 normalize_env_value
与失败日志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 20:58:02 +08:00
Nexus Agent ca0de7d04d docs: Layer 3 模拟巡检告警验证报告
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
记录 flock 失败计数 bug 修复与 iptables 演练结果。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 20:56:21 +08:00
Nexus Agent 764703d401 fix(deploy): health_monitor flock 失败计数回传
flock 子 shell 递增后未回传 FAIL,导致 Layer 3 永不触发重启与 Telegram。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 20:53:14 +08:00
Nexus Agent b8785d1d51 docs: Layer 3 巡检 Telegram 配置后生产复查
记录用户配置 Telegram 后 env 同步、health_monitor 就绪与 cron 状态。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 20:49:13 +08:00
Nexus Agent 5b372fb8a2 docs: Layer 3 巡检部署生产验证报告
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 20:19:31 +08:00
Nexus Agent f9934bd4f1 feat(ops-patrol): Layer 3 巡检与 Telegram 配置打通
保存 Telegram 时同步至 .env,health_monitor 支持 MySQL 回退,
设置页展示巡检状态,部署时自动安装 cron。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 20:17:24 +08:00
Nexus Agent 66a4cba39b docs: 设置页 Telegram 修复生产验证报告
记录 540a7fc 部署结果与浏览器终验步骤。
2026-06-07 19:46:43 +08:00
Nexus Agent 540a7fce05 fix(settings): Telegram 检测/测试闭环与 8 项告警开关恢复
检测与测试前自动持久化 Token/Chat ID;getUpdates 清除 Webhook 并返回具体 API 错误;恢复 NOTIFY 分项开关与免密 reveal。
2026-06-07 19:45:07 +08:00
Nexus Agent 2e911dcca7 fix(frontend): 设置页恢复 IP 白名单订阅 URL 与解析预览
补回代理订阅配置 UI,修正 ip-allowlist 保存 payload 为 manual_ips + subscription_url。
2026-06-07 19:34:24 +08:00
Nexus Agent a5d5518054 fix(frontend): 设置页恢复 Telegram Chat ID 自动检测
Vue SPA 迁移遗漏 getUpdates 检测按钮;对接 GET /settings/telegram/chats,并修正 cron 脚本 bash 调用。
2026-06-07 19:31:13 +08:00
Nexus Agent e3fe161ae5 fix(deploy): 健康检查 PORT 缺省 8600
NEXUS_PUBLISH_PORT 未写入 .env.prod 时避免空端口误打 OpenResty 导致部署脚本误报失败。
2026-06-07 18:05:27 +08:00
Nexus Agent d0be2fdcf1 fix(deploy): SSH secrets 加载、Docker 运行时检测与外网探测脚本修复
使 deploy-production.sh 可读本机 secrets 并在非 docker 组用户下用 sudo 识别 1Panel 栈;修复 security_probe_external.sh 的 curl shift 误用与 WebSocket 自动 venv。
2026-06-07 17:34:27 +08:00
Nexus Agent 72d82d737b fix(security): BL-07 WS 403 澄清与 code-review 跟进加固
外网无 token WebSocket 的 HTTP 403 为应用层拒绝而非反代故障;边端 agent
在中心 401 时停止心跳重试;install 锁定路由级 404 且归档失败 fail-closed。
同步安全规范与 BL-06 一致,门控 7/7 PASS。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 12:12:49 +08:00
Nexus Agent eab35a35be fix(security): BL-06 强制 Agent per-server key 并合并心跳单次查库
移除 Agent 认证与安装链路的 global API_KEY 回退,收口 risk-acceptance 接受项 2;巡检补修心跳重复 get_server 并加 assert_called_once 回归测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 11:17:41 +08:00
Nexus Agent e5e2582743 fix(security): 外网攻击面加固 BL-01~05
收紧 PUBLIC 路径误匹配、强制 health/detail 与壁纸 sync 鉴权、默认关闭 OpenAPI;
安装锁定后 /api/install/* 统一 404;附探测脚本、测试与审计文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 01:45:10 +08:00
Nexus Agent 722917bfc9 fix(api): API 全量巡检 B1–B6 修复 server_ids 上限与 pending 审计
限制健康检查/推送/脚本执行的 server_ids 规模,补齐 pending 重试失败与删除审计,并附分批巡检报告。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 23:57:46 +08:00
Nexus Agent ac17f91106 docs(audit): 14 页 API 契约审计与凭据轮询 SSOT 收尾
记录凭据轮询/pending 契约对照与生产验证,更新功能指南与附录 J,并将 pending 相关类型集中到 api.ts。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 23:50:05 +08:00
Nexus Agent 9ee5c8a708 docs(project): nx 上帝菜单巡检 round1–10 归档总结
新增 nexus-god-menu-audit-summary.md 汇总巡检交付与日常速查,并链入文档索引。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 23:25:44 +08:00
Nexus Agent fb9dbcb11a docs(deploy): 同步 1Panel 运维 SSOT(round4–8 能力)
README 与知识文档补全 nx cron/备份/验收;卸载脚本提示改为 1Panel 容器名。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 23:23:27 +08:00
Nexus Agent 72e46cef32 fix(deploy): 升级后镜像验收与 verify 脚本修复
验收检查容器内 round6/7 特性;修复 host.docker.internal 引号;nx verify 与升级后自动验收。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 23:21:44 +08:00
Nexus Agent e5d482264d fix(install): 向导守护 UI 与升级后 cron 交互提示
Docker 完成页展示 Layer 3 cron 步骤与 host_deploy_root;TTY 下 nx update 可一键安装巡检/备份 crontab。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 23:20:23 +08:00
Nexus Agent c9e08799f9 fix(install): 守护配置对齐宿主机 cron 与验收检查
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
Docker 向导提示 nx cron/宿主机路径;裸机 cron 含 db_backup;验收脚本检查 cron 与 MySQL 备份能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 23:11:02 +08:00
Nexus Agent 89865ec690 fix(deploy): 部署脚本自动识别 Docker 与 Supervisor 运行时
upgrade/deploy-production 等分流到 nexus-1panel upgrade;升级完成后提示安装 nx cron。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 23:07:47 +08:00
Nexus Agent ba69bc3355 fix(deploy): git pull 后备份脚本不依赖可执行位
db_backup/backup_mysql 改为检测文件存在并用 bash 调用;install_ops_cron 一并 chmod 备份脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 22:39:10 +08:00
Nexus Agent 49f159a8ce fix(deploy): install_ops_cron 自动安装 cron 包
生产 Azure 主机默认无 crontab,安装脚本现会 apt install cron 并启用服务。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 22:38:43 +08:00
Nexus Agent 4e1347ba45 fix(deploy): Docker 感知 MySQL 备份与 nx cron 安装
升级/定时备份通过 docker exec 1Panel MySQL 容器 dump,不再依赖宿主机 mysqldump;nx 菜单新增巡检与备份 crontab 安装。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 22:37:49 +08:00
Nexus Deploy 61b8d7d419 fix(deploy): health_monitor Docker 适配与巡检第三轮修补
Docker 生产用 docker restart;读 .env.prod 端口与容器 Telegram;已归档跳过全量热同步。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 22:25:27 +08:00
Nexus Deploy 954bdbd972 fix(deploy): 验收脚本已安装时输出正确通过文案
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 21:54:03 +08:00
Nexus Deploy 314bece418 fix(deploy): 验收脚本已安装时跳过 install API 检查
锁定后 /api/install/* 返回 403 属预期,不应判 FAIL。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 21:53:43 +08:00
Nexus Deploy 687e059a86 fix(deploy): 上帝菜单巡检第二轮 — 全新安装卷冲突与验收修正
FRESH 清除 nexus-state 旧锁;PENDING=1 时 entrypoint 不恢复旧 env;验收脚本已安装期望 404。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 21:53:03 +08:00
Nexus Deploy baacdb0252 fix(deploy): 已归档安装向导时 sync 脚本验收期望 404
避免 nx update 因 install.html 已移除而误报失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 21:31:06 +08:00
Nexus Deploy 58bdf820dc fix(install): 安装锁定后将 install.html 归档为 .bak
防止已安装环境继续提供安装向导静态页;entrypoint 与升级脚本避免容器重建后恢复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 21:30:15 +08:00
Nexus Deploy dd66d99f2d fix(deploy): nx 上帝菜单巡检修补 1panel-network 与升级竞态
purge 阶段 compose 叠加 1panel overlay;菜单启动/重建与回滚后校验网络;install-auto 已安装改走 update。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 21:27:01 +08:00
Nexus Deploy 107b089b16 fix(1panel): 安装锁持久化与升级后 1panel-network 校验
防止 nx update 重建容器后丢失安装锁、未接入 1panel-network 导致 MySQL/Redis 不可达。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 20:52:13 +08:00
Nexus Deploy 1f5daebeed fix(deploy): nx update 在 set -u 下 local root 同行展开报错
拆分 nexus_publish_port/set_install_complete/sync_env_prod_to_volume 的 local 声明,
避免 env_file="$root/..." 在 root 赋值前展开导致 unbound variable。
2026-06-06 20:38:11 +08:00
Nexus Deploy 4ba45abf38 feat(servers): 凭据轮询登录与连接失败列表
- 密码/SSH密钥预设增加用户名;快速添加(IP)轮询全部预设尝试 SSH 登录
- 成功入 servers 列表;失败写入 pending_servers 并支持重试/删除
- 新增 credential_poller、try_ssh_login 与 add-by-ip/pending API
2026-06-06 20:08:48 +08:00
Nexus Deploy 5a56e5a8cb fix(1panel): 安装向导脱敏、配置一致性与 nx update 门控回滚
- /state 白名单脱敏;init-db db_port 修复;connection-check 后清空 redis_pass
- install.html sessionStorage token 与无 token 友好提示
- nx update: 镜像 import 门控、健康失败自动回滚、PENDING=0 回写、卷双写
- Redis 容器改名密码迁移;备份失败默认 WARN;update.sh 管道 ROOT 回退
2026-06-06 17:40:30 +08:00
Nexus Deploy 850038a3ec fix(install): 安装向导 Redis 密码不被 nx update/Compose 覆盖
Compose 不再注入 NEXUS_REDIS_URL;nx update 保留卷内带密码 URL;步骤 4 可从 install state 修复无密码 URL。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 11:34:26 +08:00
Nexus Deploy beb802802a chore: 添加 scripts/git-push.sh 与本地 Gitea secrets 约定
push 读取 deploy/nexus-1panel.secrets.sh(gitignore),clone/pull 仍可匿名。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 11:22:07 +08:00
Nexus Deploy 476d87c678 docs: Gitea 公共仓库无需凭据
更新 .cursorrules 与 linux-dev-paths,明确不配置/探测 GITEA token。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 11:13:07 +08:00
Nexus Deploy 49f88a2cd8 fix(install): Docker .env 持久化与 entrypoint 覆盖 Compose 占位符
向导 init-db 后立即同步 nexus-state 卷;entrypoint 跳过不完整 env 并在启动前 source /app/.env,避免缺 DATABASE_URL 与无密码 REDIS_URL 导致容器崩溃。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 11:08:22 +08:00
Nexus Deploy b11360015d fix(install): 1Panel Redis 无账号与步骤 4 URL 自愈
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
确立 Redis 仅密码认证(:pass@/default),隐藏误导性用户名字段;
connection-check 自动修正错误 NEXUS_REDIS_URL,并更新运维文档与验收脚本。
2026-06-06 07:51:52 +08:00
Nexus Deploy 3b2856f388 fix(install): auto-resolve 1Panel Redis auth (:pass@ / default)
Stop prefilling root; probe multiple Redis URL formats on credential
check and persist the URL that works for init-db.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 07:15:57 +08:00
Nexus Deploy 9d9fbc62f5 docs: add 1Panel operations knowledge SSOT and Redis test script
Consolidate install wizard, networking, URL formats, and troubleshooting
into docs/project/nexus-1panel-operations-knowledge.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 07:08:42 +08:00
Nexus Deploy 807f4c2448 fix(install): correct Redis URL auth for 1Panel root user
Password must use redis://:pass@ or redis://root:pass@ host; wizard adds
redis_user field defaulting to root on 1Panel Docker installs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 06:59:45 +08:00
Nexus Deploy da061d35db feat(install): require MySQL/Redis credential check at wizard step 3
Add test-credentials API and UI gate before init-db writes .env.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 06:47:22 +08:00
Nexus Deploy d0544c9bd2 fix(1panel): grant nexus@% for Docker MySQL 1045 on install
Add fix-1panel-mysql-grant.sh and clearer install wizard errors when
1Panel MySQL only allows nexus@localhost.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 06:19:19 +08:00
Nexus Deploy 3f5eacae60 feat(deploy): add 1Panel install wizard verify script
Server-side checklist for 1panel-network, container host sync, and
install API defaults before completing /app/install.html.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 06:05:15 +08:00
Nexus Deploy b25d0798f6 fix(1panel): sync MySQL/Redis container hosts on nx update
Upgrade was skipping 1panel-network detection; wizard now overrides stale host.docker.internal from volume/API defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:47:43 +08:00
Nexus Deploy 6b1d23c7d7 fix(1panel): sync Redis URL to container name on 1panel-network
Update NEXUS_REDIS_URL when detecting 1Panel-redis-*; add Redis TCP precheck and connection errors matching MySQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:39:52 +08:00
Nexus Deploy 6951b8049f fix(1panel): join 1panel-network and connect MySQL/Redis by container name
Per 1Panel App Store architecture, bridge apps must use Docker DNS on 1panel-network instead of host.docker.internal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:35:04 +08:00
Nexus Deploy da0424a268 fix(install): MySQL TCP probe and clearer host.docker.internal errors
Step 2 now detects whether host MySQL is listening; init-db fails fast with 1Panel setup guidance on error 2003.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:22:16 +08:00
Nexus Deploy 569263dddb fix(install): remove redundant Redis/MySQL host hints in wizard step 3
Docker defaults already prefill host.docker.internal; drop duplicate 1Panel install reminders from the form.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 05:08:55 +08:00
Nexus Deploy 646929ddff feat(deploy): register nx globally via install-nx-cli.sh
Centralize chmod and /usr/local/bin symlinks; install, upgrade, update, and every nx invocation refresh the global nx command.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:43:00 +08:00
Nexus Deploy 0a7c5ee617 fix(deploy): set executable bit on nx and refresh symlinks on update
Git tracked deploy/nx as 644 so /usr/local/bin/nx failed with Permission denied after pull; update.sh now chmods CLI scripts before upgrade.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:39:33 +08:00
Nexus Deploy 93400b7793 fix(install): split Redis install check (step 2) from connection test (step 4)
Step 2 only TCP-probes host Redis without blocking the wizard; step 4 runs /connection-check on MySQL and Redis from .env before creating the admin account.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:32:47 +08:00
Nexus Deploy a5aa5f81ad docs(deploy): merge nx god into unified menu and expand README
Consolidate install and ops into a single nx menu with one-click update via update.sh; document all curl install/update entry points in the root README.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:27:11 +08:00
Nexus Deploy 86c865e710 feat(deploy): add update.sh and always rebuild on upgrade
Upgrade no longer exits when Git is already up to date; it still rebuilds the Nexus image to pick up entrypoint changes. Add deploy/update.sh as the one-command entry point.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 04:16:15 +08:00
Nexus Deploy 1bd69077a2 fix(deploy): auto-remove legacy Compose MySQL containers on upgrade
Run uninstall-mysql-compose before install/upgrade and pass --remove-orphans so nexus-prod mysql orphans are stopped after the service is dropped from compose.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 03:32:00 +08:00
Nexus Deploy f37ebf8621 feat(docker): remove bundled MySQL from production Compose stack
Production installs now run only the Nexus container; MySQL and Redis are self-hosted on the machine or 1Panel, with install wizard defaults pointing at host.docker.internal.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 03:27:57 +08:00
Nexus Deploy e42dc77127 feat(deploy): add redis compose uninstall; require Redis in install wizard
Step 2 blocks until Redis connects; uninstall-redis-compose.sh removes legacy
nexus-prod-redis container without touching host/1Panel Redis.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:51:52 +08:00
Nexus Deploy 536d520744 fix(nx): resolve deploy dir when invoked via /usr/local/bin symlink
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
readlink -f fixes sourcing nexus-1panel.sh; register nexus-1panel global cmd.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:49:34 +08:00
Nexus Deploy 6276ea08b7 feat(docker): remove bundled Redis; use external host install
Compose stack is MySQL+Nexus only; wizard Redis check is non-blocking
and defaults to host.docker.internal for self-managed Redis.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:48:00 +08:00
Nexus Deploy c81fe3183f fix(deploy): sync install.py into container for Redis env-check hotfix
sync-install-wizard-to-container.sh now copies server/api/install.py and
restarts Nexus so Docker installs pick up redis:6379 without full rebuild.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:34:53 +08:00
Nexus Deploy 5e7c5beb43 fix(install): probe redis via docker service host in env-check
Install wizard no longer hardcodes 127.0.0.1:6379; Docker installs get
mysql/redis prefills and a passing Redis check inside the nexus container.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:31:11 +08:00
Nexus Deploy 9b7dba99ff fix(install): drop Gitea token warning for public repo installs
Public anonymous clone needs no secrets file; document token as optional
for private repos only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:20:56 +08:00
Nexus Deploy 222703d733 fix(install): allow curl-pipe bash when set -u is enabled
BASH_SOURCE[0] is unset for stdin scripts; call main directly in
install-1panel-docker.sh and quick-install.sh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:11:40 +08:00
Nexus Deploy d884ea00ca feat(install): add full-stack install-1panel-docker.sh for fresh servers
Replace the nx redirect stub with a real orchestrator that chains 1Panel
setup, Docker Compose checks, and install-nexus-fresh; document UI-only
OpenResty proxy rules in README-1panel.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 02:00:24 +08:00
Nexus Deploy 422dcae0ff fix(docker): bake install.html into prod image with build verify and sync script 2026-06-06 01:14:59 +08:00
Nexus Deploy 343def7e77 fix(docker): include install.html and wizard vendor assets in prod image 2026-06-06 00:36:03 +08:00
Nexus Deploy 83ce11f55c feat(install): per-host auto-generated secrets for Docker installs
Each fresh 1Panel/Docker install generates unique MYSQL and NEXUS keys
via scripts/generate_nexus_secrets.py; install wizard reuses compose env.
2026-06-06 00:25:57 +08:00
Nexus Deploy e613aecc8e docs: clarify Gitea web URL vs git clone URL in README 2026-06-06 00:19:04 +08:00
Nexus Deploy f71da47ccf docs: add root README for Gitea product page and install scripts 2026-06-06 00:14:34 +08:00
Nexus Deploy 1acac6439a feat(deploy): public repo quick-install via curl (1Panel style)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 00:11:43 +08:00
Nexus Deploy feda614c52 fix(deploy): register nx/nexus-fresh in PATH for root
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:58:37 +08:00
Nexus Deploy 6cef3d2942 fix(deploy): private Gitea raw 404 — use clone or token API download
Anonymous curl saved "Not found." causing "Not: command not found";
add download-install-fresh.sh, --skip-clone, and script self-check.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:52:48 +08:00
Nexus Deploy 00f44032ff feat(deploy): add 1Panel-style fresh install script for empty DB
install-nexus-fresh.sh with phased checks, install wizard URL, and nx god
menu hints; wired into nx install-fresh and install menu option 6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:44:38 +08:00
Nexus Deploy 8ddcf2ad0d feat(deploy): add nx interactive installer and god ops menu
Single entry deploy/nx for install wizard, post-install restart/upgrade/logs,
and install-auto for non-interactive 2c8g setup on 1Panel Docker hosts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:41:04 +08:00
Nexus Deploy 65e4478b4b feat(deploy): load credentials from nexus-1panel.secrets.sh
Keep secrets out of tracked scripts; support git add -f secrets file,
git remote token parsing, and port/firewall checks for one-click install.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:37:37 +08:00
Nexus Deploy 2f830424a4 feat(deploy): unified 1Panel script with profiles and port checks
Merge install/upgrade into nexus-1panel.sh with 1c4g/2c8g/4c16g resource
profiles; preflight Gitea and firewall hints; post-install HTTPS health probe.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 22:59:47 +08:00
Nexus Deploy 7b33b745a0 feat(deploy): add 1Panel Docker one-click install script
Automates clone, docker/.env.prod, compose up, and health check for 2C8G prod stack.
2026-06-05 22:52:44 +08:00
Nexus Deploy bfda70bbe7 chore(docker): tune production stack for 2C8G hosts
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
Add mysql-prod-2c8g.cnf, container mem/cpu limits, Redis maxmemory,
and default Nexus pool 30/20 for 8GiB 1Panel deployments.
2026-06-05 22:44:06 +08:00
Nexus Deploy 2f67e9e2b9 docs: add 1Panel Docker production deploy stack
Add Dockerfile.prod, docker-compose.prod.yml, OpenResty example,
and migration plan for Baota to 1Panel + Compose deployment.
2026-06-05 22:40:04 +08:00
Nexus Deploy 811eb97fbf chore: Ubuntu local dev scripts, deploy gate, and audit changelogs
Rename WSL helpers to Linux-native scripts, expose Redis in docker-compose,
prefer .venv tools in pre_deploy_check, and record 2026-06-04 audit waves.
2026-06-04 23:02:21 +08:00
Nexus Deploy 0100ab2582 docs: consolidate archive, SSOT guide, and agent entry stubs
Move line-walk/delta/phase2/frontend audits and legacy memory into
docs/archive/, add functional development SSOT, thin AGENTS/CLAUDE stubs,
and regenerate changelog/audit indexes via consolidate_docs.py.
2026-06-04 23:01:03 +08:00
Nexus Deploy b866e944ab fix: Code Review P0/P1 batches 1-3 and single-operator risk acceptance
Apply sync/install/auth/schedule/retry/agent/settings fixes from full
code review; document accepted WS and Agent legacy risks for solo ops.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 15:57:49 +08:00
Nexus Deploy a2ae74d582 release: BUG fixes batch 1-6, full bug scan docs, Ubuntu/Linux dev
- Backend: auth refresh reuse, schedule/retry/sync/agent/files fixes
- Frontend: push WS, files ETag browse, vite build assets
- Docs: audit/changelog, production deploy gate, bug scan registry B7-77
- Deploy: deploy-production.sh, prune assets, gate logs
2026-06-04 14:01:14 +08:00
Your Name 9ac6a2d27f fix(docker): track docker/.env.example despite .env.* gitignore
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 00:59:00 +08:00
Your Name 3cec226b89 feat(docker): add Compose stack for MySQL, Redis and Nexus API
- Dockerfile with healthcheck and entrypoint for deps + .env persistence
- generate_env.py for local secrets; design docs and changelog

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 00:58:46 +08:00
Your Name 51948469c2 build(frontend): track Vite output in Git for Gitea deploy
- Stop ignoring web/app/index.html and web/app/assets/
- Bundle index-DBKIQT7H.js from current vite build (~101MB assets)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 00:52:44 +08:00
Your Name 4b5602a719 feat(files): fix editor freeze, EOL gates, server form and session UX
- FilesPage storeToRefs + remotePath coercion; Monaco preload; list action buttons
- text_io for UTF-8/LF; install/sync EOL; check_shell_eol + check_text_eol gates
- ServerFormDialog, hash login redirect, AppAuth keep-session; design docs and audits

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 00:42:55 +08:00
Your Name 9c9c763e9c fix(credentials): password preset is name+password only, no username field
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 16:08:50 +08:00
Your Name 0cc1cc14a4 fix(terminal): restore app-bar top inset so tab bar and xterm are not clipped
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 16:02:17 +08:00
Your Name f078d0e03b feat(push): localize sync/rsync error messages to Chinese in history and progress
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 15:18:01 +08:00
Your Name 524dabcf7f refactor(editor): P0 — per-tab model, EOL preservation, conflict detection (409)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 14:46:49 +08:00
Your Name 0ccd1ba934 fix(files): use proxyRefs for inject context unwrapping
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 08:25:25 +08:00
Your Name a6673157a5 fix(files): unwrap injected refs via reactive context for v-model
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 08:23:03 +08:00
Your Name 78798e3630 feat(push): refactor B0-B6, /ws/sync channel, staging cleanup
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 08:14:09 +08:00
Your Name 68fdcbae0a refactor(terminal): split TerminalPage into composables and components (T1-T6)
Extract session/WS/xterm logic with markRaw native store, per-session disconnect overlay, shared server picker, and cmd bar via ShellHighlightField expose.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 06:44:56 +08:00
Your Name 181560eca2 fix(ui): align servers stat cards with dashboard; improve files browse errors
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 06:08:02 +08:00
Your Name c6f1d73ff5 feat(files): P4 GET browse with ETag and P5 zero-flicker loading
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 05:59:55 +08:00
Your Name 6cb591212d refactor(files): P2 split UI into components with page context inject
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 05:14:13 +08:00
Your Name 7cbddf5d48 refactor(files): P1 split composables, Pinia store, directory cache
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 05:05:26 +08:00
Your Name cb738a8e9a fix: file manager double browse from v-select init and stale chunks
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 04:36:46 +08:00
Your Name 5393252036 fix: eliminate double browse requests in file manager
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 04:29:11 +08:00
Your Name 3bc2843642 fix: browse序号保护防止token刷新重试导致的双刷新 2026-06-02 03:48:01 +08:00
Your Name 1d6d26d612 fix: 加载中隐藏空状态提示,消除进目录时的视觉双刷新 2026-06-02 03:19:27 +08:00
Your Name c65e3ea2a7 fix: 目录双击导致双刷新(e.detail守卫),横幅仅在实际权限错误时显示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 03:07:51 +08:00
Your Name 67dac0168f feat: 文件管理器复制粘贴守卫、2MB编辑限制、一键配置sudoers、编辑器外观重构
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 02:20:22 +08:00
Your Name 57d67c494b fix: 警告横幅关闭后进子目录不再重新出现,文案简化
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:51:13 +08:00
Your Name 928e3c5fdb fix: 登录白名单 IP 绕过防暴破锁定
白名单内 IP 跳过失败次数锁定;失败仅审计不计数;成功登录清除该用户历史失败记录。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:37:22 +08:00
Your Name 832e34fd98 fix: 修复 6 个 MEDIUM 级 Bug (锁定/重试/调度/分页/SSH池/token_version)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:29:06 +08:00
Your Name 19cfb7caaa fix: 系统全扫描修复 7 个 HIGH 级 Bug
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:22:00 +08:00
Your Name ea3046f635 fix: unix_ls parse_ls_la_line 兼容 long-iso 8字段格式
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:09:39 +08:00
Your Name 39a8cf16cf fix(ui): 调度与重试队列路由导航及前端资源一致性
侧栏可滚动与显式导航;懒加载失败提示;getList 兼容分页响应;生产 prune 清理 1065 个过期 chunk。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 22:36:08 +08:00
Your Name b62d4f6f9f fix(audit): 审计日志操作与目标类型全面中文化
补全 action/target 映射表与词段兜底;审计页筛选下拉使用中文标签。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 22:18:42 +08:00
Your Name 44c19f329a fix(terminal): Ctrl+D 快捷键文案改为退出
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 22:13:54 +08:00
Your Name 9e924562d2 fix(terminal): 右下角快捷键与右键菜单中文化并放大
快捷键区改为中文大按钮;工具栏字号/回滚/全屏中文化;右键菜单加大并在命令栏拦截英文系统菜单。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 22:00:49 +08:00
Your Name e3eb0e65a5 fix(ui): 仪表盘/审计/调度/重试修复与终端快捷命令改版
统一终端页与全局侧栏菜单;快捷命令仅在终端执行,增删改排序迁入系统设置;命令输入区加高与 Enter/Shift+Enter 行为;后端自定义命令优先与 reorder API。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 21:56:07 +08:00
Your Name fc056059f9 docs: production deploy changelog for files/POSIX release
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 20:45:14 +08:00
Your Name bd6467dff9 docs: master plan execution progress and changelog
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 19:58:50 +08:00
Your Name e704b01093 fix(deploy): restore pre_deploy_check.sh content
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 19:57:06 +08:00
Your Name 246070e599 fix(deploy): pre_deploy gate uses repo root and newest audit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 19:56:36 +08:00
Your Name d93c518a14 feat(files): POSIX paths, elevation, recursive chmod, access hints and docs
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 19:54:18 +08:00
Your Name 1cfac23c01 feat: terminal ANSI colors and enhanced shell syntax highlighting
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:49:27 +08:00
Your Name 799140f6c4 feat: terminal SPA layout, route sync, and WebSocket URL helper
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:41:59 +08:00
Your Name 8935081ac5 feat: full-site acceptance automation and health/detail fix
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:22:42 +08:00
Your Name ab431028c1 fix: 服务器页批量选择绑定 Vuetify4 modelValue
v-model:selected 无法同步勾选状态;生产已部署 ServersPage-CGp8DUVa.js 并验证「已选择 2 台服务器」。
2026-06-01 14:54:39 +08:00
Your Name cc2dad7f31 docs: 生产浏览器验收 — SPA/WebSSH/health_monitor
详见 docs/reports/2026-06-01-browser-verification.md;批量条与 Telegram/Push/kill 留维护窗口或人工确认。
2026-06-01 14:20:45 +08:00
Your Name 24b4439b2b docs: 单负责人执行计划 — handoff 对齐、生产验收、prune 加固
计划见 docs/design/plans/2026-06-01-single-owner-execution.md;生产 health/app/Redis/MySQL 已通过。
2026-06-01 14:09:17 +08:00
Your Name b8af1fc418 chore: 移除 superpowers 工作流 Skill,改为单 Agent 协作
停用 .cursor/skills 与虚拟部门分派,新增 no-department-agents 规则并更新文档索引与验收标准。
2026-06-01 14:04:50 +08:00
Your Name b77a314b36 chore: remove virtual department agent docs (team/skills)
Delete docs/skills/, docs/team/, and TEAM_ROLES.md. Update docs/README and mem_key_files. Standards and .cursor/skills workflows unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 13:52:55 +08:00
Your Name d519113734 fix: Gate3 health plain text, refresh empty body, prune underscore chunks
test_api: assert /health returns plain text ok instead of JSON parse.

auth: RefreshRequest.refresh_token optional so SPA POST {} is not 422.

prune: allow leading underscore in asset names to avoid deleting _plugin-vue_export-helper.

Add run_test_api_on_server.sh and deployment follow-up changelog.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 13:41:28 +08:00
Your Name 65c77155d8 fix: 修复前端 assets 清理脚本以支持 Rolldown 产物
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 12:31:55 +08:00
Your Name 2deb89b49c deploy: 前端部署后自动清理孤儿 assets + 冒烟脚本
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 12:29:57 +08:00
Your Name b76295aaec docs: 记录 2026-06-01 生产部署验证结果
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 12:23:44 +08:00
Your Name 8311821590 fix: 全量修复批量选择、JWT 60min、script-callback 认证与心跳数据流
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 12:17:47 +08:00
Your Name 73fdcafb81 docs: 完整交接文件 (15章节 + Cursor提示词 + 部署问题)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:58:11 +08:00
Your Name 91d0d4104e docs: 添加 .cursorrules 供 Cursor 接管
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:54:11 +08:00
Your Name 7fa4bd6dff docs: Cursor交接文件 2026-06-01
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:49:12 +08:00
Your Name a20cefcfbd docs: 审计+changelog 全站bug审查修复
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:40:23 +08:00
Your Name 6183047fd6 fix: 全站 bug 审查修复 (前端9项 + 后端5项)
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
前端修复:
- P0: App.vue http 未导入导致全局搜索崩溃
- P1: TOTP 登录流程断裂 (TotpRequiredError 非 ApiError 子类)
- P1: ServersPage 编辑服务器 domain 被端口字符串污染
- P1: PushPage Set[0] 无效 → values().next().value
- P1: SettingsPage API 返回字符串未转数字
- P2: api/index.ts getList 重复定义移除
- P2: LoginPage 锁定倒计时 now ref + watch 更新
- P2: TerminalPage 路径验证正则放松 (允许空格/中文)
- P2: useWebSocket CONNECTING/CLOSING 状态关闭旧连接

后端修复:
- P1: websocket.py redis.keys() → scan_iter() 避免阻塞
- P1: auth_service.py TTL 仅在 key 无 TTL 时设置
- P1: servers.py 批量操作加 asyncio.Lock 避免并发 session commit
- P2: settings.py asyncio.get_event_loop() → get_running_loop()
- P2: settings.py datetime.utcfromtimestamp() → datetime.fromtimestamp(tz=utc)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:24:55 +08:00
Your Name df2c188f5d docs: Cursor 交接文件
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:32:10 +08:00
Your Name 2d00d6a8eb fix: 登录页居中布局 + 删除Logo区域
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:30:12 +08:00
Your Name 080c1158d4 docs: 审计文档 + changelog (TerminalPage迭代)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:24:53 +08:00
Your Name facc1e8ae4 feat: TerminalPage全面迭代 + 快捷命令MySQL持久化
P0: Ctrl+L清屏、命令历史恢复、重连per-session、onopen不忽略、ping per-session
P1: 空catch处理、termRefs用session.id、错误透传、剪贴板权限、webssh-token错误
P2: 全选菜单、指数退避重连、快捷命令编辑、uptime per-session
新功能: quick_commands MySQL表 + CRUD API、Shell语法高亮输入栏
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:12:18 +08:00
Your Name 1ecd8e4542 fix: move wallpaper routes before /{key} catch-all to fix 404
Bing wallpaper endpoint (/bing-wallpapers) was returning 404 from HTTP
because FastAPI matched it against the /{key} catch-all route at position
2 before the specific route at position 13 ever got reached.

- Moved wallpaper routes (bing-wallpapers, bing-wallpaper) and helper code
  before the /{key} wildcard route
- Removed dead guard code in get_setting() that was never effective —
  FastAPI route matching happens before the handler body executes
- Removed duplicate wallpaper code block at end of file

Root cause: FastAPI matches routes in registration order. /{key} at
position 2 matched 'bing-wallpapers' as the key parameter, returning
'Setting not found' instead of routing to the wallpaper endpoint.
2026-05-31 22:00:33 +08:00
Your Name c6485168b6 fix: bing-wallpapers route was being caught by /{key} catch-all
FastAPI matches routes in order: /{key} registered before bing-wallpapers
because routes are sorted by path. /{key} matches 'bing-wallpapers' as a
key name, returning 404 'Setting not found' instead of hitting the actual
bing_wallpapers handler.

Fixed by adding a guard in get_setting() to skip these fixed path names,
and crucially by ensuring bing-wallpaper(s) routes are registered BEFORE
the /{key} catch-all in the router.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:59:26 +08:00
Your Name e9af02dd22 fix: add bing-wallpapers (plural) to PUBLIC_PREFIXES for JWT bypass 2026-05-31 20:53:49 +08:00
Your Name c9baecfdfa feat: Bing wallpaper slideshow — 12 images, hourly rotation, weekly cleanup
- Frontend: fullscreen wallpaper background with 1.5s crossfade transition,
  login card centered with glass effect, images rotate every hour
- Backend: new /api/settings/bing-wallpapers endpoint fetches 8 recent images
  from Bing HPImageArchive, caches to web/app/wallpapers/, returns all URLs
- Auto-cleanup: removes files older than 7 days, keeps max 12 newest
- Old /bing-wallpaper endpoint kept for backward compatibility

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:51:50 +08:00
Your Name 3d5870b404 fix: correct wallpaper dir to 3 levels up (server/api/settings.py -> Nexus/web/app/wallpapers)
Path breakdown: __file__ is server/api/settings.py
  .parent → server/api/
  .parent.parent → server/
  .parent.parent.parent → Nexus/ (repo root)
  Then append web/app/wallpapers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:38:35 +08:00
Your Name afd32118b2 fix: correct wallpaper directory path in settings.py
- Path(__file__) is server/api/settings.py, so:
  .parent → api/  .parent → server/  .parent → Nexus/ (ROOT)
  .parent → (above ROOT — wrong!)
- Changed from .parent.parent.parent to .parent.parent.parent.parent
  to reach the project root, then into web/app/wallpapers/
- Moved _WALLPAPER_DIR.mkdir into the async function body

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:34:27 +08:00
Your Name d0308aaef6 fix: allow /app/wallpapers/ through AppAuthMiddleware
- Added /app/wallpapers/ to _APP_PUBLIC_PREFIXES so cached wallpaper images
  are served without requiring login auth
- Wallpaper files are now saved to web/app/wallpapers/ by bing_wallpaper() endpoint

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:30:46 +08:00
Your Name 04dda2c419 fix: download Bing wallpaper to server disk, auto-clean weekly
- Changed bing-wallpaper endpoint from URL proxy to image downloader
- Saves wallpaper to web/app/wallpapers/YYYY-MM-DD.jpg
- Auto-deletes wallpapers older than 7 days on each request
- Falls back to yesterday's cached file if download fails
- Added Path import for filesystem access

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:23:55 +08:00
Your Name e084d90c61 fix: add Bing wallpaper proxy endpoint, make it public (no JWT required)
- Added GET /api/settings/bing-wallpaper — proxies cn.bing.com HPImageArchive
  to avoid CORS issues, returns {url} for the daily wallpaper
- Added /api/settings/bing-wallpaper to PUBLIC_PREFIXES in auth_jwt.py
  so the login page can fetch it without authentication
- Login page now fetches wallpaper via backend proxy instead of direct CORS-blocked fetch

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:12:09 +08:00
Your Name 24aa8494e5 security: bind uvicorn to 127.0.0.1 instead of 0.0.0.0
The install wizard wrote '--host 0.0.0.0' which exposed the raw uvicorn
port 8600 on the public IP, bypassing Nginx HTTPS/TLS/WAF/middleware.
Changed to 127.0.0.1 — only local Nginx can reach the backend.

Also applied to running server via supervisor config fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:04:28 +08:00
Your Name 5c7775c10c chore: remove old Tailwind HTML pages from git
Vuetify SPA is now the sole frontend entry point at /app/.
Old pages (alerts/servers/scripts/...html) replaced by Vue router.
Keep install.html for setup wizard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:46:29 +08:00
Your Name 7a6479dbfa fix: remove stale login.html from public paths, clean up prefixes
Old Tailwind HTML pages removed from server — Vuetify SPA is now
the sole entry at /app/. Only install.html remains for setup wizard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:44:56 +08:00
Your Name 0fdae981cf fix: add ttf/eot/otf to static asset whitelist in AppAuthMiddleware
Vuetify/Monaco fonts return 404 without these extensions whitelisted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:42:43 +08:00
Your Name 3419ab8a09 chore: remove obsolete design department agent docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:37:57 +08:00
Your Name d512460dc9 fix: frontend type definitions match actual backend API responses
- AlertsPage: template fields alert_type/value/is_recovery (was type/message/recovered)
- CommandsPage: remove non-existent is_dangerous/duration/admin_name; use admin_username
- ScriptsPage: ExecHistoryItem matches /scripts/executions response shape
- types/api.ts: CommandLogItem + SshSessionItem + ScriptExecItem aligned to backend
- Remove unused PaginatedResponse imports from CommandsPage/ScriptsPage

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:37:41 +08:00
Your Name 0f02e047cc docs: update deployment instructions for Vuetify SPA
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
- Split backend/frontend deploy steps
- Add deploy-frontend.sh one-liner reference
- Document AppAuthMiddleware 404 and stale HTML troubleshooting
- Remove obsolete dist copy instructions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 13:25:33 +08:00
Your Name 35583115ff fix: Vite outputs directly to web/app/ — no dist copy needed
- vite.config.mts: outDir changed from web/app/dist to web/app
- .gitignore: ignore web/app/index.html + web/app/assets/ (build artifacts)
- deploy/deploy-frontend.sh: automated build+deploy+verify script

Deployment is now: bash deploy/deploy-frontend.sh

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 13:24:22 +08:00
Your Name f745714530 fix: allow Vuetify SPA index.html through AppAuthMiddleware
The new Vuetify SPA uses hash routing — all pages served from single
index.html. The old middleware only whitelisted individual HTML page
paths (login.html etc.), blocking the SPA entry point with 404.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 13:13:13 +08:00
Your Name 0d056a45e9 feat: Vuetify 4 frontend — complete rebuild with audit fix
Vue 3 + Vuetify 4 + TypeScript + Vue Router 4 + Pinia frontend,
replacing the old Tailwind+Alpine.js static pages.

Infrastructure (8 new files):
- composables/useSnackbar.ts — centralized notifications
- composables/useServerList.ts — server dropdown data
- composables/useServerPagination.ts — paginated server table
- types/api.ts — 14 typed API response interfaces
- types/global.d.ts — Window augmentation
- utils/status.ts — server status display helpers
- utils/validation.ts — 8 form validation rules
- components/MonacoEditor.vue — fullscreen code editor

14 pages rebuilt:
- LoginPage, DashboardPage, ServersPage, TerminalPage
- FilesPage, PushPage, ScriptsPage, CredentialsPage
- SchedulesPage, RetriesPage, CommandsPage
- AlertsPage, AuditPage, SettingsPage

Critical fixes (from audit):
- Files upload JWT auth (was missing Authorization header)
- API Key reveal password verification dialog
- 6 silent catch blocks → snackbar error feedback
- 33 as any → 0 (typed interfaces + global.d.ts)
- Dashboard: alerts/summary/categories/audit data loading
- WebSocket reconnect timer cleanup + auth failure handling
- Terminal ResizeObserver leak fix
- PushPage timer cleanup on unmount
- FilesPage path double-slash fix (joinPath helper)
- Filter changes reset pagination to page 1

Features added:
- Servers: batch operations, CSV export/import, detail panel
- Push: sync modes, ZIP upload, WebSocket real-time progress, cancel
- Scripts: quick execute panel, execution status tracking
- Files: Monaco editor, context menu, batch delete, rename, chmod
- Terminal: right-click menu, server sidebar, tab persistence
- Settings: batch save, TOTP manual key, password TOTP verification
- Dashboard: category distribution, recent audit, WS refresh

Quality:
- vue-tsc --noEmit zero errors
- vite build passes (1613 modules)
- Formal 8-step security audit passed (0 FINDING)
- .gitignore: dist/ → **/dist/ to cover web/app/dist/

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 12:56:12 +08:00
Your Name 0ded4a28ec fix: 主题系统改用CSS变量 — dark:前缀在浏览器CDN不生效
根因: Tailwind v4 浏览器CDN(tailwindcss-browser.js)不处理外部CSS的
@custom-variant指令, dark:前缀规则完全不生成

方案: 改用CSS变量 + :root.dark {} 选择器
- theme.css: :root 定义亮色值, :root.dark 定义暗色值
- 15个HTML页面: 恢复 bg-[var(--bg-page)] 等CSS变量class
- 移除所有 dark: 前缀class(CSS变量自动适配)
- theme-init.js: 同步读localStorage设.dark class(FOUC防护)
- layout.js: toggleTheme 切换 .dark class + localStorage + MySQL

优势: CSS变量方案不依赖Tailwind dark:变体, 100%兼容浏览器CDN
2026-05-31 04:18:55 +08:00
Your Name 5ac4a5c7cf fix: theme.css 加 @import tailwindcss 使 dark: 变体生效
Tailwind v4 官方文档确认:
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));

浏览器CDN build需要 @import 来处理 @custom-variant 指令
2026-05-31 04:13:26 +08:00
Your Name 9c5ebdfdbd feat: 全站主题完全重做 — CSS变量→Tailwind dark: 前缀
设计依据: HyperUI/Flowbite/daisyUI/Meraki UI 深度调研
配色方案: HyperUI 全局色值映射 (gray-* 色系)

改动:
- theme.css: 重写为 @custom-variant dark 配置
- theme-init.js: FOUC防护 — 同步读localStorage设.dark class
- layout.js: 主题系统改用 .dark class + localStorage
- 15个HTML页面: 649处CSS变量class替换为dark:前缀
  bg-[var(--bg-page)] → bg-gray-50 dark:bg-gray-950
  bg-[var(--bg-card)] → bg-white dark:bg-gray-900
  bg-[var(--bg-surface)] → bg-gray-100 dark:bg-gray-800
  border-[var(--border)] → border-gray-200 dark:border-gray-700
  text-[var(--text-primary)] → text-gray-900 dark:text-white
  等9种映射
- design spec: docs/design/specs/2026-05-31-ui-redesign-design.md

修复: FOUC(切换主题时闪暗色)已解决
2026-05-31 04:11:17 +08:00
Your Name c457cb4058 feat: dark mode theme system with synchronous FOUC prevention
- Add vendor/theme-init.js for synchronous dark mode init in <head>
- Update all 13 frontend pages with dark: variant classes
- Dark mode as default, light mode via localStorage toggle
- Remove redundant theme.css rules in favor of Tailwind dark: classes
- Add settings-dark-theme.png reference screenshot

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 03:54:30 +08:00
Your Name 8b2263916c feat: design department agents integrate Tailwind ecosystem knowledge (9 sites)
Deep-learned 9 Tailwind CSS ecosystem sites and injected into all design
department agents via knowledge graph references:

- HyperUI (350+ components, Tailwind v4, Alpine.js)  best match
- Meraki UI (198 components, Application UI, RTL)
- Flowbite (56+ components, data-attr driven, Figma)
- daisyUI (65 semantic components, 35 themes)
- UIBak (200+ components, Alpine.js native)
- Headless UI (a11y benchmark, data-* patterns)
- Tailblocks (63 landing page blocks)
- Kutty (plugin architecture reference, unmaintained)
- WindyToolbox (resource directory)

Updated 10 files: design-department, ui-designer, ux-architect,
frontend-designer, color-curator, inspiration-analyzer, trend-researcher,
moodboard-creator, design-wizard, team roles.

Knowledge graph IDs for cross-session reference:
  tailwind-css-9 (overview)
  tailwind-css-9/hyperui-nexus-tailwind
  tailwind-css-9/meraki-ui-rtl-tailwind
  tailwind-css-9/flowbite-tailwind-bootstrap
  tailwind-css-9/daisyui-tailwind
  tailwind-css-9/uibak-alpinejs
  tailwind-css-9/headless-ui-tailwind-labs
  tailwind-css-9/tailblocks-landing-page
  tailwind-css-9/kutty-tailwind-plugin-alpinejs
  tailwind-css-9/windytoolbox-tailwind

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 03:53:40 +08:00
Your Name 293a0d64fe feat: 全站亮色/暗色主题切换
实现方案: CSS自定义属性 + Tailwind arbitrary values

新增文件:
- web/app/vendor/theme.css — CSS变量定义(亮色+暗色)

修改文件 (15个HTML + 2个JS + 2个Python):
- layout.js — 侧边栏🌙主题切换按钮 + loadTheme/toggleTheme
- settings.py — MUTABLE_KEYS加theme/editor_theme
- 15个HTML页面 — bg-slate-*替换为bg-[var(--bg-*)] + 引入theme.css

替换统计:
- index.html: 48处
- servers.html: 183处
- files.html: 94处
- push.html: 157处
- scripts.html: 83处
- credentials.html: 100处
- schedules.html: 59处
- retries.html: 40处
- commands.html: 57处
- alerts.html: 47处
- audit.html: 32处
- settings.html: 125处
- terminal.html: 89处
- install.html: 72处
- login.html: 14处
总计: 1199处替换

功能:
- 侧边栏底部🌙/☀️按钮切换亮色/暗色
- 主题偏好保存到MySQL(settings表)
- 页面加载时自动恢复主题
- Monaco编辑器主题独立控制(⚙️菜单)
2026-05-31 02:55:45 +08:00
Your Name 94fbdf7b2d refactor: 主题切换移到⚙️设置下拉菜单
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
- 移除标题栏独立主题按钮(🌙/☀️)
- 新增⚙️设置按钮 → 下拉菜单
- 菜单项: 「☀️ 亮色主题」/「🌙 暗色主题」动态文字
- 点击菜单外自动关闭
- 菜单样式: bg-slate-800, rounded, shadow, 最小宽度200px
2026-05-31 02:42:24 +08:00
Your Name e65c9993a2 feat: 编辑器亮色/暗色主题切换 + 保存到MySQL
- 标题栏🌙/☀️按钮切换 Monaco 主题
- 'vs'(亮色) / 'vs-dark'(暗色)
- 切换后立即应用到所有打开的标签页
- 通过 PUT /api/settings/editor_theme 保存到MySQL
- 页面加载时自动读取保存的主题偏好
- settings.py MUTABLE_KEYS 白名单加 editor_theme
2026-05-31 02:39:21 +08:00
Your Name ae47eb42de fix: 双击标题栏最大化 + Monaco用官方默认主题
1. 双击编辑器标题栏空白区域 → 切换最大化/还原
2. Monaco移除theme:'vs-dark'和fontFamily覆盖, 使用官方默认配置
3. 编辑器外观与 https://microsoft.github.io/monaco-editor 一致
2026-05-31 02:35:52 +08:00
Your Name c570801119 fix: 编辑器拖拽修复 — 扩大拖拽区域+退出最大化+边界限制
问题:
- 标签栏整体被排除导致可拖拽区域几乎为零
- 最大化/全屏后拖拽不工作
- 没有拖拽视觉提示

修复:
- 添加⠿拖拽把手图标(cursor-grab)
- 只排除button/input/select, 标签栏空白处可拖拽
- 拖拽时自动退出最大化/全屏模式
- 边界限制(不拖出屏幕)
- 拖拽结束触发Monaco重新layout
2026-05-31 02:31:56 +08:00
Your Name cba8b3d8f6 feat: 浮动编辑器 — 拖拽+缩放+全屏+文件树
功能:
- 拖拽: 按住标题栏拖动编辑器面板
- 缩放: 右下角拖拽调整大小
- 最大化(): 占满屏幕留边距
- 全屏(⤢): 完全覆盖页面
- 最小化(—): 收到底部标签栏
- 关闭(✕): 关闭全部标签
- 文件树(🌳): 左侧可折叠文件树, 点击文件打开/目录导航
- 文件树同步: 浏览新目录时自动更新

布局:
┌──────────────────────────────────┐
│ [🌳] [📄a.conf×][📄b.py×]  [][⤢][—][✕] │ ← 可拖拽
├────────┬─────────────────────────┤
│ 📂 ..  │                         │
│ 📁 dir │    Monaco Editor        │
│ 📄 a   │                         │
│ 📄 b   │                         │
│        │ Ln1,Col1|ini|UTF-8|file │
└────────┴─────────────────────────┘  ← 右下角可缩放
2026-05-31 02:26:25 +08:00
Your Name caef4a216b fix: Monaco配置完全对齐官方默认值
对齐项目:
- cursorBlinking: smooth→blink(官方默认)
- 移除: cursorSmoothCaretAnimation, cursorWidth(非官方默认)
- 移除: lineHeight, padding(非官方默认)
- 移除: renderLineHighlight, smoothScrolling(非官方默认)
- 移除: scrollbar自定义尺寸(使用官方默认)
- scrollBeyondLastLine: false→true(官方默认)
- renderWhitespace: selection→none(官方默认)
- 添加: fontFamily(Cascadia Code/Fira Code/JetBrains Mono)
- 移除: quickSuggestions, insertSpaces, foldingHighlight, minimap side/scale
- 保留vs-dark主题(匹配Nexus暗色UI)
2026-05-31 02:20:04 +08:00
Your Name 507b957b4e refactor: 编辑器改为浮动面板 + Monaco配置对齐官方
布局:
- 全屏覆盖 → 右下角浮动面板(70vw×65vh)
- 文件列表始终可见, 编辑器不遮挡
- 最大化/还原按钮()
- 最小化到底部标签栏
- 移除文件树(文件列表已在背景可见)

Monaco配置对齐官方demo:
- fontSize 13→14, lineHeight 20
- wordWrap on→off(水平滚动)
- 新增: folding, matchBrackets, cursorBlinking smooth
- 新增: cursorSmoothCaretAnimation, smoothScrolling
- 新增: padding, renderLineHighlight all, renderWhitespace selection
- 新增: suggestOnTriggerCharacters, quickSuggestions
- 新增: links, colorDecorators
- 新增: tabSize 4
2026-05-31 02:14:56 +08:00
Your Name c70424a224 docs: Monaco IDE编辑器 changelog + 审计报告 2026-05-31 02:08:31 +08:00
Your Name e8c1acba9f fix: 审计8个FINDING修复 (IDE编辑器+终端)
- H-01 [MEDIUM] esc() 加引号转义('"\') 防止onclick XSS
- H-02 [MEDIUM] initialPath 路径白名单校验(仅允许字母数字/.-_)
- H-03 [LOW] doNewFile 文件名白名单正则(替代简单的includes('/'))
- H-08 [LOW] Monaco CDN SRI — 已知限制, 版本锁定@0.45.0
- H-13 [MEDIUM] showModal 移除已删除的modalTextarea引用
- H-20 [LOW] initialPath仅首次连接cd, 不影响恢复的标签
- H-26 [MEDIUM] esc()跨文件一致性 — files.html对齐terminal.html
- H-30 [LOW] esc()注释更新
2026-05-31 02:06:42 +08:00
Your Name 34786737b4 fix: IDE编辑器语法错误 — 多余花括号 2026-05-31 01:56:15 +08:00
Your Name be633f3e58 feat: IDE编辑器重构 — 多标签+文件树+最小化+状态栏
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
- 全屏IDE面板替代模态框编辑器
- 多文件标签页: 独立Monaco实例, 切换保持状态
- 左侧文件树: 基于当前目录, 点击文件打开, 点击目录导航
- 最小化/恢复: 底部最小化栏显示所有打开的文件标签
- 状态栏: 光标位置+语言+编码+未保存指示器
- Ctrl+S保存 + 未保存关闭确认
- 标签栏: 点击切换, ×关闭, amber色未保存标记
- 文件树同步: 浏览新目录时自动更新文件树
- 模态框保留用于非编辑操作(重命名/删除/新建等)
2026-05-31 01:50:34 +08:00
Your Name 00a3309005 feat: 服务器列表加「文件」按钮直达文件管理
- servers.html: renderActions 加「文件」链接(琥珀色) → /app/files.html?server_id=X
- files.html: 支持 server_id URL参数, 自动选中服务器并浏览 /www/wwwroot
2026-05-31 01:33:52 +08:00
Your Name 3d15200cfa chore: 移除Monaco debug日志 2026-05-31 01:28:57 +08:00
Your Name d8aca5bf18 fix: Monaco预加载移入files.html(解决api.js缓存问题) 2026-05-31 01:26:59 +08:00
Your Name cb60af4483 debug: Monaco preload 加日志诊断 2026-05-31 01:22:45 +08:00
Your Name 5a5adafb39 feat: Monaco编辑器+右键菜单+排序筛选+chmod+压缩解压+新建文件
编辑器增强:
- E-2 Monaco集成: 70vh全屏编辑器, 暗色主题, bracket着色
- E-3 Ctrl+S保存快捷键 + 未保存指示器
- E-4 语言自动检测(30+扩展名映射)
- Monaco预加载: 登录后CDN后台加载, 打开编辑器秒开
- fallback: Monaco未就绪时退回textarea

文件管理增强:
- F-1 右键菜单: 预览/下载/重命名/权限/复制路径/终端/压缩/解压/删除
- F-2 新建文件按钮(📄+)
- F-3 chmod权限修改(右键→输入权限如755)
- F-4 排序切换(名称/大小/时间)
- F-5 文件类型筛选下拉(.conf/.log/.sh/.py等)
- F-6 压缩(tar.gz)
- F-7 解压(tar.gz/zip)
2026-05-31 01:10:22 +08:00
Your Name c0296b9d8e fix: 预加载文件大小 30KB → 1MB 2026-05-31 01:04:44 +08:00
Your Name ca4aceba3e fix: 面包屑点击事件委托 — 作用域不在fileListEl内
面包屑(#breadcrumbPath)和文件列表(#fileList)是兄弟节点, 不是父子关系
事件委托绑在fileListEl上无法捕获面包屑点击
修复: 给#breadcrumbPath加独立click事件委托
2026-05-31 00:57:58 +08:00
Your Name b5dfa8a870 feat: 文件管理器集成SSH终端
- files.html: 工具栏加「🖥 终端」按钮(绿色),点击在新标签页打开SSH终端
- files.html: openTerminal() 传递 server_id + 当前路径到 terminal.html
- terminal.html: 支持 path URL参数,连接成功后自动 cd 到指定目录
- 打开方式: /app/terminal.html?server_id=8&path=/www/wwwroot
- 右键菜单也会集成「打开终端」选项
2026-05-31 00:53:00 +08:00
Your Name 17cdc96bb0 feat: 文件预加载+缓存 — 双击秒开编辑器
- api.js: Monaco编辑器CDN预加载(登录后后台加载)
- files.html: 文件内容预加载(目录浏览后自动预读≤30KB文本文件)
- files.html: 文件缓存(_fileCache), 双击已缓存文件直接打开
- files.html: 切换服务器时清空缓存
- 预加载策略: 最多10个文件, 仅文本类型, ≤30KB
2026-05-31 00:46:04 +08:00
Your Name 6012af0b4d fix: 双击编辑器冲突 — 延迟区分单击/双击 2026-05-31 00:25:02 +08:00
Your Name 9bdf7e420f feat: 文件管理器4项改进
1. 切换服务器自动重置路径为 /www/wwwroot
2. 默认目录 /www/wwwroot(而非根目录 /)
3. 双击文件打开编辑器(doPreview textarea模态框)
4. 双击目录进入目录(browseDir)
2026-05-31 00:17:22 +08:00
Your Name 40d672fdbb fix: 面包屑双斜杠bug (/tmp显示为//tmp) 2026-05-31 00:11:47 +08:00
Your Name 557c8d28c1 docs: files迭代审计报告 2026-05-30 23:46:12 +08:00
Your Name 9f7be66356 fix: 审计5个FINDING修复 (files迭代)
- H-15 [MEDIUM] download端点加100MB大小限制
- H-02 [LOW] Content-Disposition filename sanitize(替换特殊字符)
- H-12 [LOW] FileDownload/FileRead/FileWrite path加max_length=4096
- H-27 [LOW] doPreview保存失败解析错误详情显示detail
- H-29 [INFO] esc()函数加注释说明用途
- H-01 [HIGH] 远程路径限制 — ACCEPTED_RISK(用户明确决策)
2026-05-30 23:44:35 +08:00
Your Name ac38442383 docs: files.html 全面迭代 changelog 2026-05-30 23:36:49 +08:00
Your Name 14131f98f0 feat: files.html 全面迭代 — 15项改进
安全+后端:
- P2-1 符号链接解析(l权限检测+name/target分离)
- P2-7 新增 /api/sync/download 端点(SFTP流式下载)
- P2-8 新增 /api/sync/read-file + /write-file 端点(base64编码避免shell注入)

前端全面重写:
- P1-1 目录删除二次确认(自定义模态框+输入目录名)
- P2-2/2-3 事件委托替代内联onclick(data-action+data-path)
- P2-4 上传进度条(XHR.upload.onprogress)
- P2-5 文件大小可读化(B/KB/MB/GB)
- P2-6 文件类型图标(30+扩展名映射)
- P2-7 下载按钮+逻辑
- P2-8 文件预览/编辑弹窗(读取+修改+保存)
- P2-9 自定义模态框替代prompt/confirm
- P2-10 批量选择+批量删除(checkbox+全选+底部操作栏)
- P3-1 文件名搜索框(前端实时过滤)
- P3-2 符号链接🔗图标+target tooltip
- P3-3 空目录状态(图标+上传引导按钮)
- P3-4 服务器刷新按钮
- P3-5 操作loading状态(进度条) 2>&1
2026-05-30 23:34:24 +08:00
Your Name 64503c0260 fix: IP校验改用endpoint级HTTPException替代Pydantic validator
model_validator/field_validator在自定义RequestValidationError handler下
无法正确序列化为JSON,导致500。改用endpoint级_validate_ip_list()辅助函数
抛HTTPException(400),避免Pydantic→FastAPI序列化链路问题。
2026-05-30 22:48:41 +08:00
Your Name d19f5af807 fix: IP格式校验用field_validator替代model_validator
model_validator(mode='after')抛出ValueError时FastAPI不能正确序列化为422
→ 导致500 TypeError: Object of type ValueError is not JSON serializable
改用field_validator(mode='before')由Pydantic正常捕获返回422
2026-05-30 22:44:26 +08:00
Your Name 463841a740 docs: settings安全加固审计报告 2026-05-30 22:35:40 +08:00
Your Name 36fde6e6ed fix: 审计7个FINDING修复
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
- F-1 [MEDIUM] IpAllowlistSaveRequest 加 IP/CIDR/域名格式校验
- F-2 [MEDIUM] _is_private_host 改为 async DNS 解析 (不再阻塞事件循环)
- F-3 [LOW] hot-reload int转换失败改 logger.warning 而非静默pass
- F-4 [LOW] 提取 _verify_reauth 辅助函数,消除4处重复re-auth代码
- F-5 [LOW] ChangePasswordRequest.totp_code 加 min_length=6 约束
- F-6 [LOW] revealTelegramToken 空catch改 console.error+toast
- F-7 [LOW] toggleApiKeyVisibility 空catch改 console.error+toast
2026-05-30 22:34:45 +08:00
Your Name 1a61fae590 docs: settings 安全加固+UX迭代 changelog 2026-05-30 22:02:45 +08:00
Your Name a50f17941d fix: settings 安全加固 + UX 迭代 — 10项修复
安全修复:
- S-01 SSRF: parse-subscription 加私有IP封禁+重定向限制+响应大小限制
- S-02 值校验: PUT /{key} 加 key 白名单 + INT 范围校验(1-1000/1-100/10-600)
- S-05 重登录: 改密码成功后跳转login.html+绿色提示
- S-07 审计: add_manual_ips/remove_allowlist_ip 补全审计日志
- S-09 TOTP: 已启用TOTP时改密码需输入验证码

UX改进:
- UX-01: 密码框focus ring + 设置项input type映射(number/url/text)
- UX-02: 改密码按钮loading状态(disabled+提交中...)
- UX-03: 3处空catch块→console.error+toast
- UX-04: 保存失败时reloadSettings恢复原值
- UX-05: IP格式校验(前端正则+后端Pydantic model_validator)
2026-05-30 21:58:13 +08:00
Your Name 362b28199f docs: settings 巡检 + Bot Token 脱敏 changelog 2026-05-30 21:18:34 +08:00
Your Name 5213def150 fix: Telegram Bot Token 脱敏 — GET 不返回明文,reveal 需密码验证
- 后端 SENSITIVE_KEYS 加入 telegram_bot_token,GET /settings/ 返回脱敏值
- 新增 POST /settings/telegram/reveal-token 端点(密码验证 + 审计日志)
- 前端 Telegram 区域独立渲染:Bot Token 默认脱敏,点"显示"需输入密码
- Chat ID 保持可编辑(非敏感信息)
- Bot Token 输入框默认 type=password,可切换明文/遮掩
2026-05-30 21:12:41 +08:00
Your Name 5166660f3a fix: GET /api/settings/ip-allowlist 被 /{key} 路由拦截 → 404
/{key} 路由在 ip-allowlist 之前定义,FastAPI 将 ip-allowlist
当作 setting key 查找,找不到返回 404。

修复:将 GET /ip-allowlist 路由移到 /{key} 之前(FastAPI 路由优先级)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:42:42 +08:00
Your Name 48ef06babb chore: gitignore 添加 MCP 工具目录 + 清理测试截图
- .megamemory/ (知识图谱)
- .playwright-mcp/ (浏览器状态/截图)
- tmp/ (临时文件)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:39:47 +08:00
Your Name ede7fbaa64 fix: settings PUT 后热更新内存 — 无需重启即生效
问题:修改 Telegram 等设置后需要重启服务器才能生效,
因为 load_settings_from_db() 只在启动时运行。

修复:PUT /api/settings/{key} 写入 DB 后,
同步更新 settings 内存对象(DB_OVERRIDE_MAP 映射 + INT_SETTINGS 类型转换)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:28:51 +08:00
Your Name 4c4d41b8f5 docs: add changelog and audit for ruff cleanup
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:10:28 +08:00
Your Name 32feb1b6db fix: 全站 ruff 清零 — 77 errors → 0
Fixes:
- F821: install.py site_url 未定义(NameError)、sync_v2.py os 未导入、script_jobs.py LOG f-string
- F401: 移除 22 个未使用导入(models/__init__.py、install.py、auth.py 等)
- B904: 20 个 except 块 raise 添加 from e/from None
- S110: 20 个有意的 try-except-pass 添加 noqa 注释(SSH清理/DDL幂等/WS断连)
- B007: 3 个未使用循环变量重命名(dirs→_dirs、server_id→_server_id)
- F541: 3 个无占位符 f-string 修正
- F841: auth.py 未使用 ip_address 变量移除
- S105: auth_service.py Redis key prefix 误报 noqa
- B023: script_execution_flush.py 循环变量绑定修复

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:07:45 +08:00
Your Name ebe276ea29 docs: add changelog and audit for servers iteration
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:50:07 +08:00
Your Name 84282e87ae feat: servers list iteration — server-side pagination/search/sort/status filter
Backend:
- server_repo.py: get_paginated() adds search/sort_by/sort_order/is_online params
  with whitelist-validated sorting and DB-level pagination
- servers.py: list_servers() adds search/sort_by/sort_order/is_online Query params
  with Redis post-filter for is_online accuracy
- servers.py: fix all 12 pre-existing ruff errors (unused imports, B904, S110, F541)
- servers.py: fix F821 bug — agent_port undefined in upgrade_agent()

Frontend:
- servers.html: complete rewrite with server-side pagination, debounced search,
  clickable column sorting, status filter dropdown, auto-refresh toggle,
  keyboard shortcuts (↑↓/jk/Esc//r), CSV export, skeleton loading,
  empty state with contextual hints, pagination controls (first/prev/nums/next/last)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:45:53 +08:00
Your Name de8d860244 fix: handle legacy agent heartbeat without server_id + migrate on_event to lifespan
- AgentHeartbeat.server_id changed to Optional[int] — missing server_id
  now returns 200 (discarded) instead of 422, stopping retry loops
- web/agent/agent.py: on_event("startup") → FastAPI lifespan pattern
- Added exponential backoff on consecutive heartbeat failures (cap 5min)
- Agent stops heartbeat loop on "discarded" response from central
- main.py: promoted temporary debug 422 handler to production-grade
- uninstall.sh: added legacy agent cleanup (pkill patterns, crontab,
  /opt/multisync-agent path)
- Confirmed servers.html batch buttons work correctly (disabled state
  is correct when no agents installed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:20:14 +08:00
Your Name 3f50a40d25 feat: Agent 安装/卸载/升级脚本全面迭代
install.sh:
- 新增 Step 0:停旧服务 + 杀端口占用(fuser/ss/netstat 兜底)
- 修正步骤编号 [1/5] → [0/7]
- pip 依赖版本提取为脚本顶部变量,方便统一维护

uninstall.sh:
- 加 pkill 杀残留 Agent 进程
- 加 fuser 杀端口占用
- 清理 /etc/sudoers.d/nexus-agent 残留
- root 用户跳过 sudo 前缀

_sudo_wrap:
- NOPASSWD 命令白名单替代 ALL=(ALL)(仅 systemctl/apt/yum/rm 等)
- 降低 SSH 超时后 sudoers 残留的风险

升级 API:
- restart 前加 fuser -k 杀端口占用(单台+批量)
- install_agent_remote 检查 stdout 中的 FAILED 标记

卸载后清理:
- 清空 agent_api_key(之前只清 agent_version)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:11:09 +08:00
Your Name dc48f71b25 feat: 创建 Agent 卸载脚本 uninstall.sh
之前卸载功能调用了不存在的 uninstall.sh,导致 404 下载失败。
脚本功能:停止 systemd 服务 → 禁用并删除 unit → 删除 /opt/nexus-agent → 删除日志
支持非 root 用户(自动检测 sudo 权限)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 14:35:33 +08:00
Your Name 913fd16bb9 fix: Agent 升级命令添加 sudo 前缀(非 root 用户)
systemctl restart 需要 root 权限,非 root SSH 用户必须加 sudo。
之前 _sudo_wrap 只设置了免密 sudoers 但命令本身没带 sudo,
导致升级时 "Interactive authentication required" 失败。
同时修复单台升级和批量升级的 rollback 命令。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 14:28:49 +08:00
Your Name deffa8a8f4 fix: AppAuthMiddleware 验证 refresh token 格式而非 JWT 解码
refresh token 是 opaque 格式 (token:admin_id:token_version),不是 JWT。
之前用 pyjwt.decode() 验证必然失败,导致所有登录用户访问 /app/ 都返回 404。
改为验证格式合法性(3段、admin_id>0、token_version>=0)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 05:10:39 +08:00
Your Name 64a8a18287 fix: refresh cookie path 改为 /,修复 AppAuthMiddleware 读不到 cookie
REFRESH_COOKIE_PATH 从 /api/auth 改为 /,这样浏览器访问 /app/ 页面时
也会发送 nexus_refresh cookie,AppAuthMiddleware 才能正确验证登录状态。

同时包含之前的 AppAuthMiddleware 定义顺序修复和 422 调试日志。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 05:08:21 +08:00
Your Name cc0ca47d61 debug: 422 handler 简化 — 只记录 errors 和 content_type,不读 body
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 03:09:30 +08:00
Your Name 449373f24f debug: 添加 422 验证错误日志 — 记录请求体和验证错误详情
诊断 Agent 心跳 422 问题,临时捕获请求体内容。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 03:05:30 +08:00
Your Name 730b665306 fix: AppAuthMiddleware 类定义移到 add_middleware 调用之前
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
Python 模块级代码顺序执行,类必须先定义再引用。
之前 add_middleware(AppAuthMiddleware) 在第404行但类定义在第495行,
导致 NameError 服务启动失败。同时修复 _is_install_mode → is_install_mode。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 03:01:08 +08:00
Your Name f5bea6f0d2 fix: AppAuthMiddleware add_middleware 重新加回 2026-05-30 02:46:11 +08:00
Your Name c808c28edf fix: 移除错误的 AppAuthMiddleware add_middleware(类定义在后面)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:45:19 +08:00
Your Name d53309e37e fix: AppAuthMiddleware 类定义移到 add_middleware 之前
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:43:29 +08:00
Your Name a8fe7fed0a fix: 添加 REFRESH_COOKIE_NAME import,修复 AppAuthMiddleware NameError
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:41:12 +08:00
Your Name f91cd847a9 feat: AppAuth 中间件 — 未登录时 /app/ 页面返回空白 HTML 404
通过 nexus_refresh cookie 验证身份,无有效 token 返回空白 HTML,
不暴露系统存在。白名单路径: login/install/vendor/static assets。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:39:48 +08:00
Your Name 993a234152 docs: 审计 + changelog — health纯文本/sha256/AppAuth
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:37:15 +08:00
Your Name e709c72f1f docs: CLAUDE.md 新增部署流程 + Health 纯文本修复
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:30:49 +08:00
Your Name f7c4b95dd6 fix: /health 返回纯文本 "ok" 替代 JSON,隐藏 FastAPI 后端指纹
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:30:01 +08:00
Your Name 75864fe04f fix: md5 → sha256 替换文件校验哈希算法,消除 bandit CWE-327
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 02:27:18 +08:00
Your Name ae22db4b3e fix: Nginx 重定向 + 自定义 404 — 隐藏 FastAPI 后端信息
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
nginx_https.conf 新增注释化重定向规则,将裸页面路径 301 到 /app/ 前缀;
main.py 添加 StarletteHTTPExceptionHandler,404 返回空白 HTML 而非 JSON,
避免泄露后端框架标识。

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-30 02:03:57 +08:00
Your Name 0b82a3fee7 feat: 推送取消 — 引擎 cancel flag 检查 + WS cancelled 计数器
sync_engine_v2: 每台服务器推送前检查 Redis sync:cancel:{batch_id},
若已取消则跳过并标记 cancelled 状态,WS 广播 cancelled 计数。
websocket: broadcast_sync_progress 新增 cancelled 参数。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 00:57:38 +08:00
Your Name d785c78d6d fix: 审计修复 — esc() 加单引号转义 + 移除死代码 editQuickCmd
B2: esc() 不转义 ' 可能在 onclick 中注入
B4: editQuickCmd 定义但无调用(✏按钮已移除)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 00:26:14 +08:00
Your Name 56906e8fe2 fix: 快速命令栏移至命令栏上方 — chip 样式替代右侧面板列表
自定义命令在前(brand高亮),内置命令在后(灰色)。hover 显示 × 可删除。
+ 号弹窗添加新命令。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 00:22:31 +08:00
Your Name da234c4e73 fix: 终端命令栏被覆盖 — termContainer隔离终端实例
termminal div 用 absolute inset:0 覆盖了底部 cmdBar。
修复: termContainer(flex-1 relative) 容器隔离,cmdBar 作为 flex sibling 在下方。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 00:19:16 +08:00
Your Name 7193b88156 feat: 终端页面 11 项全覆盖迭代 — 多标签/快速命令/字体/历史/重连/右键等
11 项改进:
- 多标签会话 (切换/关闭/新建,每标签独立 xterm+WS)
- 快速命令栏 (内置5条 + 自定义,localStorage持久化)
- 字体大小调节 (A−/A+按钮 + Ctrl+/-)
- Ctrl+L 清屏
- 命令历史持久化 (200条 FIFO localStorage)
- 右键菜单 (复制/粘贴/清屏/全选)
- 断开自动重连 (指数退避 + 倒计时)
- 连接信息栏 (时长 + PING RTT)
- 终端路径显示 (OSC title + onTitleChange)
- 滚动缓冲区调节 (1K/5K/10K/50K)
- 快速搜索服务器

后端: PING/PONG + PROMPT_COMMAND 注入

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 00:17:22 +08:00
Your Name ac66f5b4e7 fix: 卸载Agent后清除Redis心跳(之前只清MySQL导致在线状态残留)
卸载Agent后只更新MySQL is_online=False,但前端读Redis覆盖为True。
修复: uninstall_agent_remote + batch_uninstall_agent 都增加redis.delete(heartbeat:{id})

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:57:06 +08:00
Your Name 6dd0b487b7 fix: install.sh 和 upgrade_agent 增加 rsync 自动安装
推送功能依赖目标服务器安装 rsync,但 install.sh 仅安装 Python 包。
现在 install.sh 步骤2/6检测并自动安装 rsync(apt/yum/dnf)。
upgrade_agent / batch_upgrade_agent 流程中也增加 rsync 检测,
已有服务器点击"升级 Agent"即可自动补装 rsync。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:45:01 +08:00
Your Name fe211619aa fix: 审计修复 — esc() 增加双引号转义, 移除死代码 replace(/'/g,"\'")
B1: esc() 不转义 " 导致文件名含双引号时破坏 HTML onclick 属性
B2: esc().replace(/'/g,"\'") — esc已转义单引号为&#39;,后续replace为死代码

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:07:01 +08:00
Your Name 0cbda8c840 fix: 审计修复 — diagnose/file-diff 端点静默吞错改为 logger.debug/warning
B1: diagnose 磁盘和路径检查 except:pass 改为 logger.debug()
B2: file-diff SSH失败无日志改为 logger.warning()

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:11:06 +08:00
Your Name 5958c21048 feat: 推送页面 Round 4 — 服务器搜索 / Diff对比 / 排错诊断 / 源路径输入 / 批量重试
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
5 项新功能:
- H2: 服务器搜索过滤 — 目标服务器下拉加搜索框实时过滤
- H3: 推送对比 Diff 视图 — 预览文件列表加 diff 按钮对比本地与远程差异
- H4: 推送排错面板 — 失败行加诊断按钮检查SSH/磁盘/权限
- H5: 源路径直接输入 — 支持直接输入服务器本地路径推送
- H6: 批量重试 — 一键重试所有失败服务器

新增后端端点:
- POST /api/sync/validate-source-path (H5)
- POST /api/sync/diagnose (H4)
- POST /api/sync/file-diff (H3)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:29:41 +08:00
Your Name 02423cb803 docs: 推送页面 Round 2 迭代设计文档与 changelog
- 设计文档: 5 项迭代功能(同步说明、在线标识、失败重试、Telegram通知、文件预览)
- 技术方案: 详细实现步骤与文件清单
- Changelog: 功能变更记录

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 18:54:12 +08:00
Your Name 152852e2c3 fix: 会话时效30天 + 多设备同时登录
- access token 从 30 分钟延长到 30 天
- refresh token 从 7 天延长到 30 天
- refresh token 从 MySQL 单字段改为 Redis Set 存储,支持多设备同时登录
- 单设备退出不再踢掉其他设备(只删自己的 Redis hash)
- 改密码/禁用 TOTP 仍会让所有设备掉线(token_version 递增)
- 移除 reuse 误杀逻辑:版本不匹配只拒绝当前 token,不惩罚其他设备

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 18:52:14 +08:00
Your Name 3c7036babf fix: 补回 servers.html 丢失的检测路径+卸载Agent功能
从 worktree 分支同步完整的 servers.html,补回:
- 批量操作栏"检测路径"按钮(搜索 workerman.bat 自动设置 target_path)
- 批量操作栏"卸载 Agent"按钮
- batchDetectPath() 和 batchUninstallAgent() 函数

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 16:56:23 +08:00
Your Name 74de9d303e feat: 推送页面 Round 2 迭代 — 同步说明 + 在线标识 + 失败重试 + Telegram通知 + 文件预览
F5: 同步模式详细说明 — 每种模式下方显示说明文字,切换时动态更新
F2: 服务器在线状态标识 — 服务器列表显示🟢/🔴在线/离线标识
F1: 失败自动重试 — 推送失败自动创建重试任务 + 失败行"🔄 重试"按钮
F3: 推送完成 Telegram 通知 — 全成功🟢/部分失败🟡/全失败🔴 三种消息含详情
F4: 文件内容预览 — 文件管理器点击文件名弹出内容预览模态框

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:59:30 +08:00
Your Name ad2e7667d3 Merge branch 'claude/condescending-ishizaka-3cff3a' 2026-05-28 21:02:09 +08:00
Your Name 82c297776a feat: 推送页面 5 项迭代 — WS实时进度 + 分组选择 + 历史增强 + 文件操作 + 推送校验
F1: broadcast_sync_progress() + batch_id + 前端WS逐台更新
F2: get_paginated() platform_id/node_id + /categories端点 + 筛选下拉框
F4: _sync_log_to_dict() 补全字段 + diff_summary捕获 + 可展开详情面板
F7: /local-file-ops端点 + 文件管理器删除/重命名
F8: /verify端点 md5sum对比 + 推送后校验UI

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:01:27 +08:00
Your Name 77b332c037 fix: 远程安装按钮修复 + install.sh sudo/venv兼容 + api_base_url管理
- 修复详情面板"远程安装"按钮始终禁用:s._base_url 改为从新API获取全局API_BASE_URL
- 新增 GET /api/servers/meta/api_base_url 端点
- api_base_url 加入 DB_OVERRIDE_MAP + 启动迁移逻辑
- install.sh 非 root 用户自动 sudo 检测 + NOPASSWD 配置
- 后端 _sudo_wrap 辅助函数应用于 4 个 Agent 操作端点
- install.sh venv 创建加 || true 兼容 set -e(缺 python3-venv 不再直接退出)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:33:19 +08:00
Your Name 97be1fd214 完善审计日志中文化:auth_service/script_service/sync_engine 全部 detail 改中文 + 前端新增 webssh_token/refresh_token_mismatch 等映射 + MCP deploy 修复
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:57:51 +08:00
Your Name 366bdc18fa fix: deploy tool git pull before gate check + use origin/main
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:27:32 +08:00
Your Name 280357d7d7 Merge remote-tracking branch 'origin/claude/condescending-ishizaka-3cff3a' 2026-05-27 02:19:40 +08:00
Your Name fd9b7d85b5 fix: 批量安装按钮检测 + 审计日志中文化 + terminal断开反馈
1. updateBatchBar() hasAgent 判断从 agent_api_key_set 改为 agent_version
2. 所有后端 AuditLog detail 字段统一中文
3. audit.html 操作/目标类型中文映射
4. terminal.html 断开覆盖层 + 重新连接按钮
5. Agent os_release 上报 + 前端系统版本拆分

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:17:35 +08:00
Your Name 8b82d6bab0 Add changelog for fmtTime/venv/batchinstall fixes
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:59:48 +08:00
Your Name 869f64ac18 Fix install.sh venv robustness + batch install API error reporting
install.sh: detect incomplete venv (missing activate), auto-install
python3-venv and retry instead of silently failing.

servers.py batch install: preserve server_name on exception, detect
"Status: FAILED" in stdout, return error details when stderr is empty,
increase SSH timeout to 180s.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:53:00 +08:00
Your Name 69056491a7 Fix fmtTime Invalid Date for timezone-aware timestamps (+00:00)
fmtTime() was appending 'Z' to all timestamps, causing Invalid Date
when the timestamp already had a timezone suffix like +00:00.
Now tries new Date(t) first, falls back to new Date(t+'Z').

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:34:51 +08:00
Your Name 02f1fa4ef3 Add .gitattributes + changelog for Agent install fix and deploy
- .gitattributes: enforce LF line endings for .sh/.py to prevent CRLF issues on Linux
- Changelog: document Agent install 404 fix, curl|bash pipe fix, CRLF fix, and successful deployment to both sub-servers

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:27:05 +08:00
Your Name eadf7254a1 Merge remote-tracking branch 'origin/claude/condescending-ishizaka-3cff3a' 2026-05-26 22:59:14 +08:00
Your Name 1c70e597a3 Fix Agent install 404 + add command input bar + fix install pipe error hiding
1. Mount /agent/ static files for install.sh/agent.py (was 404)
2. Replace curl|bash pipe with temp-file download to catch curl errors
3. Add command input bar to terminal.html bottom
4. Fix terminal CSS layout for flex with command bar

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 22:57:32 +08:00
Your Name 867e2aa552 Merge branch 'claude/condescending-ishizaka-3cff3a'
# Conflicts:
#	server/infrastructure/database/setting_repo.py
2026-05-26 21:58:38 +08:00
Your Name a2d392dae7 fix: WebSSH terminal echo delay — replace readline() with read()
asyncssh's async-for on shell.stdout internally calls readline()
which buffers until a newline is found. For interactive terminal
use, single-character echo never contains a newline, so all
typed characters were buffered until Enter was pressed.

Fix: use shell.stdout.read(4096) which returns data as soon as
any is available, and set bufsize=4096 on create_process() to
reduce the internal read buffer from 128KB to 4KB for low-latency
interactive echo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:54:40 +08:00
Your Name 8b10f9d64a fix: WebSSH terminal disconnect after connect + stale pool retry
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
- Fix shell creation failure on stale pooled SSH connections:
  force-close and retry with fresh connection
- Fix empty error message in logs: use {type(e).__name__}: {e!r}
- Fix double WebSocket.close() causing RuntimeError in finally block:
  track ws_closed flag to avoid closing twice
- Fix Alpine undefined error in terminal.html: $d() now returns
  default object when Alpine hasn't initialized yet
- All websocket.close() calls now wrapped in try/except

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:33:31 +08:00
Your Name b1aa235726 fix: Gate3 false positive + test_api.py production compatibility
- Gate3: Replace loose grep (matched "0/25 failed" summary) with precise
  [FAIL] marker counting + "N test(s) failed" pattern
- test_api.py: Add .env credential loading for gate check automation
- test_api.py: Fix REST status codes (POST→201, DELETE→204)
- test_api.py: Add 204 empty body handling, dynamic ID, pre-cleanup
- Add gate_log.jsonl tracking to all 7 gates
- Add knowledge graph restructure changelog

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:33:31 +08:00
Your Name 5afb18b745 feat: gate v2 — 3→7 gates with anti-bypass, lint, security, review cross-validation
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 10:02:14 +08:00
Your Name a1b0b2c514 feat: Terminal sidebar+panel, SSH key fix, auth cleanup, pre-deploy gates
- terminal.html: left sidebar (default open), right server panel with server list
- servers.py: fix SSH key double-encryption in update_server, auth method switch cleanup
- servers.html: auth switch clears opposite credentials (key↔password)
- deploy/pre_deploy_check.sh: 3-gate pre-deploy check (changelog/audit/test)
- mcp/Nexus_server.py: deploy() runs gate check before git pull+restart
- docs/audit/: audit record template + today's audit results
- CLAUDE.md: add gate enforcement rules and progress bar discipline

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 09:16:05 +08:00
Your Name f591033309 docs: add changelog for asset management removal
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:58:14 +08:00
Your Name 89fa517693 refactor: remove asset management page, simplify server form
- Delete assets.html (platform/node management had low utility)
- Remove "资产管理" sidebar entry from layout.js
- Server form: remove Platform/Node dropdowns, keep only Category
  combobox (with datalist for existing categories)
- Remove loadOrgSelects() and platform_id/node_id from save logic
- Backend API and DB tables preserved for data compatibility

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:46:39 +08:00
Your Name 973e23e038 fix: CSV template download 401 + category combobox
1. CSV template download link now uses apiFetch (with JWT) instead
   of direct <a> href which returned 401 Unauthorized
2. Server category field changed from plain input to combobox with
   datalist — users can select existing categories or type new ones

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:30:46 +08:00
Your Name ad7f4d8d62 docs: update changelog with route fix and browser verification results
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:27:25 +08:00
Your Name fb4e51ec5a fix: move static routes before /{id} to fix route conflict
/batch/install-agent, /batch/upgrade-agent, /import, and
/import/template were registered after /{id}, causing FastAPI
to match 'batch' and 'import' as the {id} parameter.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:25:25 +08:00
Your Name 8cd091add8 feat: CSV batch import, SFTP upload, batch Agent operations
Add three missing frontend management features:
1. Server CSV import (POST /api/servers/import) with injection prevention
2. SFTP file upload (POST /api/sync/upload) with path traversal protection
3. Batch Agent install/upgrade with Semaphore concurrency control

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:17:43 +08:00
Your Name 6be989c299 feat: add SSH key preset management to credentials page
Add third tab "SSH密钥预设" to credentials.html with full CRUD:
- List presets with public key preview and creation date
- Add/edit preset (name, private key, public key)
- Reveal private key via re-authentication modal
- Delete preset with confirmation
- Route "+ 新建" button to correct form based on active tab

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 23:54:11 +08:00
Your Name ccb78ed980 feat: SSH key preset system — dropdown + backend auto-resolve
- Add SshKeyPreset model + repo (encrypted_private_key, public_key)
- Add ssh_key_preset_id to Server model + schemas
- Add CRUD + reveal API routes (/api/ssh-key-presets/)
- servers.py: auto-resolve ssh_key_preset_id → decrypt → set ssh_key_private
- servers.html: key preset dropdown in key auth section, hides path/textarea on select
- Mirror password preset pattern: zero frontend credential exposure

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 22:49:57 +08:00
Your Name 95aa8a610f security: move refresh token to HttpOnly cookie
- Refresh token stored in HttpOnly + Secure + SameSite=Lax cookie
  instead of localStorage (XSS can no longer steal it)
- Login sets cookie, refresh reads from cookie, logout clears cookie
- Access token remains in localStorage (short-lived, 30min)
- Clear pendingPassword from memory after TOTP success/back
- Remove "admin" from username placeholder
- Cookie path scoped to /api/auth (minimal exposure)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 20:22:34 +08:00
Your Name 9de6ac9904 fix: auth default to password + remove SSH tab from assets
servers.html: Default auth method to "密码" (password) instead of "密钥"
(key) when adding a new server. Hide key path row by default.

assets.html: Remove SSH Sessions tab — it duplicated the server list
functionality. Assets page now focuses on classification metadata:
platform templates and node groups only.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 19:33:39 +08:00
Your Name cf0725b037 fix: credentials.html JS parse error + assets.html SSH session UX
credentials.html: Remove broken nested ternary `Alpine?Alpine.store?...`
that caused "Unexpected token ':'" which killed the entire script block,
making all functions (showAddCred, loadCreds, etc.) undefined.

assets.html: Replace SSH session history list with server list +
double-click to open terminal, matching user's requested UX.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 19:17:09 +08:00
Your Name 3943ff4f3a fix: remove updated_at secondary check that invalidated fresh JWTs
The updated_at secondary check in _verify_token() was a "defense in
depth" mechanism, but it caused a race condition: login updates admin
(last_login, onupdate→updated_at) AFTER creating the JWT, so the JWT's
"updated" claim is immediately stale. Any DB admin update (including
innocuous ones like last_login) would invalidate all existing tokens.

The token_version primary check already covers all security scenarios
(password change, TOTP disable, token reuse detection) and is the
correct mechanism for invalidating sessions. Removed the updated_at
check from both _verify_token() and get_optional_admin().

Also reordered login/refresh flows to create JWT AFTER admin update,
so the token captures the latest updated_at (belt and suspenders).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 16:23:20 +08:00
Your Name deb3a94ee6 fix: token_version NULL handling — normalize None→0 in JWT comparison
Direct DB inserts (e.g. install wizard, manual MySQL) may leave
token_version as NULL. JWT payload had tv:null which failed the
strict != comparison against DB value 0. Normalize both sides
with `or 0` for defensive coding.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 15:37:10 +08:00
Your Name 99201f48fd fix: MissingGreenlet error — convert all middleware to pure ASGI
Root cause: BaseHTTPMiddleware.call_next() runs the endpoint in a
separate async task, which breaks SQLAlchemy's greenlet context.
After session.commit(), the connection is released to the pool.
When session.refresh() tries to acquire a new connection for a
SELECT, it fails with MissingGreenlet because the greenlet spawn
context is not available in the call_next() task.

Fix (two-part):
1. Convert all 4 middleware classes from BaseHTTPMiddleware to pure
   ASGI middleware — eliminates call_next() entirely so the entire
   request chain runs in the same async context.
2. Set expire_on_commit=False on the session factory — after commit,
   objects retain their in-memory values instead of being expired.
   This removes the need for session.refresh() entirely.

All session.refresh() calls removed from 11 repository files.
With expire_on_commit=False, post-commit attribute access no longer
triggers lazy loads that would also fail with MissingGreenlet.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 15:24:15 +08:00
Your Name c72631a293 merge: 安装脚本+PHP移除+宝塔适配 合并到main
合并 claude/condescending-ishizaka-3cff3a 分支:
- deploy/install.sh: 一键安装脚本(宝塔/标准模式自适应)
- deploy/upgrade.sh + uninstall.sh
- docs/install-guide.md: 完整中文安装教程
- PHP完全移除: install.php删除, config.php→config.json
- crypto.py: 移除PHP AES-CBC兼容, 纯Fernet
- install.py: 宝塔Supervisor路径自适应, Fernet密钥生成
- install.html: Step 5宝塔/标准模式条件渲染
- 44个冲突文件使用worktree分支版本(全部最新代码)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 08:25:29 +08:00
Your Name a1276f38ad feat: 一键安装脚本 + PHP完全移除 + config.php→config.json
新增文件:
- deploy/install.sh: 7步一键安装(自动检测宝塔/标准模式)
- deploy/upgrade.sh: git pull + pip install + restart + 健康检查
- deploy/uninstall.sh: 停服务+删配置, 可选--purge删部署目录
- docs/install-guide.md: 完整中文安装教程(宝塔+手动)

核心改动:
- install.py: 宝塔Supervisor路径自适应, init-db返回bt_panel标记,
  config.php→config.json, 移除PHP锁文件, 生成Fernet加密密钥
- install.html: Step 5根据btPanel显示不同Supervisor/Nginx/SSL指引
- crypto.py: 移除PHP AES-CBC兼容层, 纯Fernet加密
- 删除 web/install.php (1192行PHP, 功能已完全迁移到Python)

PHP清理:
- Nginx配置: config\.php→config\.json deny规则
- config.py/main.py/migrations.py: install.php注释→install wizard
- CLAUDE.md/AGENTS.md: config.php→config.json, 移除PHP引用
- firefox_server.py: login.php→login.html默认URL

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 08:23:37 +08:00
Your Name 400dcae781 fix: 代码验证发现的两处问题
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
- websocket.py: asyncio.get_event_loop() → get_running_loop()
  (Python 3.10+废弃,shutdown时可能RuntimeError)
- asyncssh_pool.py: 全忙等待逻辑在锁外操作有竞态条件
  简化为直接raise ConnectionError,不在锁外重试

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-24 16:31:13 +08:00
Your Name cdd0be328a fix: 六轮深度扫描 — 47项Bug修复、安全加固、死代码清理
Critical runtime bugs:
- terminal.html WebSSH完全不可用(URL前缀/JSON解析/Content-Type三处错误)
- servers.py路由遮蔽:/logs被/{id}拦截,3个前端页面同步日志查询失败
- scripts.html startExecPoll()→startExecPolling(),长任务快速执行崩溃
- agent.py {value!r!s:.50}格式串非法,agent发非数值时ValueError
- alerts.html d.daily.reduce()无null检查,API返回空数据时TypeError

Resource leak / stability:
- websocket.py僵尸连接未关闭TCP,文件描述符泄漏
- websocket.py _last_alert_time字典无限增长(加1小时过期清理)
- asyncssh_pool.py全忙时超过MAX_CONNECTIONS无限增长
- self_monitor.py Telegram告警无冷却,宕机时每30秒刷屏
- schedule_runner.py一次性调度执行超60秒会重复触发
- 限速脚本EXPIRE每次重置窗口可绕过(改用Lua原子脚本)

Security:
- JWT access token加token_version声明,改密码后旧token立失效(零宽限)
- INSTALL_MODE导入时常量→动态函数,安装后JWT认证不再残留禁用
- install.py /lock端点加管理员存在性验证,防止阻断安装
- ServerUpdate schema移除connectivity只读字段,防止伪造连接状态

Frontend fixes:
- doExec()缺r.ok检查、commands.html null检查
- _server_to_dict()补last_checked_at+ssh_key_public
- _field_match()逗号cron表达式修复
- alerts类型显示、SSH会话名称、搜索高亮定位
- 一次性/循环定时任务(run_mode+fire_at+自动禁用)

Dead code removed (400+ lines):
- SyncService batch_push/_push_single等5个方法(零调用者)
- 5个未使用schema(SyncCommands/SyncConfig/SyncSftp/FileDeploy/PaginatedResponse)
- 6个零调用service方法、3个无前端API端点
- 4个未使用import

Schema migrations:
- push_schedules: run_mode + fire_at列,cron_expr改NULL
- servers: 7个新列 + ssh_key_private/public VARCHAR(500)→TEXT

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-24 16:26:40 +08:00
Your Name fbf5eadcf2 fix(security): R2+R3 全面代码审计修复 — 3 CRITICAL / 9 HIGH / 15 MEDIUM / 10 LOW
CRITICAL:
- C-1: sync_service.py rsync命令shlex.quote防Shell注入回归
- C-2: agent.py exec端点危险命令正则拦截+compare_digest+僵尸进程修复
- C-3: WebSSH JWT token绑定server_id防IDOR

HIGH:
- H-1: 所有PUT/POST端点setattr改为字段白名单防任意字段注入
- H-2: settings/assets/scripts端点添加JWT认证
- H-3: refresh_token invalidate-then-generate防并发重放
- H-4: servers.py Redis pipeline解决N+1查询
- H-7: settings API Key/Secret掩码+禁止修改
- H-8: TOTP暴力破解速率限制(5次/5分钟)
- H-9: get_optional_admin改用request-scoped session

MEDIUM:
- M-2: X-Real-IP优先+X-Forwarded-For取末值防IP伪造
- M-4: asyncssh异常信息脱敏(只含server_id)
- M-5: 分页limit上限(500/200/500/500)
- M-9: API层Redis依赖抽象至dependencies.py
- M-10: WebSocket ConnectionManager多worker Redis聚合
- M-13: login_attempts复合索引
- M-14: ServerRepository.get_by_ids批量查询
- M-15: API错误消息统一中文

R3追加:
- sysctl命令注入防护+regex验证
- $DB_PASS明文存储→masked_command双命令方案
- MYSQL_PWD环境变量替代-p flag
- health端点拆分(公开/需认证)
- repo层**kwargs→字段白名单
- 全局str(e)信息泄露修复(API/Telegram/WebSocket)
- SSRF私有IP拦截
- Nginx HTTPS配置+DB备份脚本
- 前端CDN SRI哈希+XSS修复
- __import__替换为正常import

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:58:33 +08:00
Your Name 9bba58a529 feat: Telegram test+ChatID, Agent IP allowlist/upgrade, alert history page
Telegram:
- POST /api/settings/telegram/test: send test message (checks token+chat_id)
- GET /api/settings/telegram/chats: call getUpdates, return up to 5 chats
- settings.html: test button + detect chat IDs with click-to-fill

Agent IP allowlist (web/agent/agent.py):
- allowed_ips config: list of trusted IPs/CIDRs from config.json
- _ip_allowed(): checks exact IP, CIDR (ipaddress module), hostname
- verify_api_key(): now checks IP allowlist before API key
- /health: also checks IP allowlist
- install.sh: auto-extracts central server IP from --url, adds to allowed_ips

Agent auto-upgrade (server/api/servers.py):
- POST /api/servers/{id}/upgrade-agent: SSH → curl new agent.py → systemctl restart
- servers.html: 升级 Agent button in Agent tab (only when online)

Alert history (new page):
- domain/models: AlertLog table (server_id, type, value, is_recovery, created_at)
- migrations.py: CREATE TABLE IF NOT EXISTS alert_logs
- websocket.py: _save_alert_log() called from broadcast_alert/recovery
- settings.py: GET /api/alert-history/ (paginated, filters), GET /stats
- main.py: register alert_history_router
- alerts.html: new page with stats cards, top-servers bar chart, alert list
- layout.js: 🔔 告警中心 added to sidebar nav

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 18:27:12 +08:00
Your Name 488838c989 feat: separate subscription/manual IPs + master enable/disable toggle
Data model (separate storage):
- login_allowlist_enabled: 'true'/'false' master switch
- login_subscription_url: URL to fetch every 2h
- login_subscription_ips: last fetch result (replaced entirely each refresh)
- login_manual_ips: manually added (never overwritten by refresh)
- login_allowed_ips: combined (used by auth for legacy compat)

ip_allowlist_refresh.py:
- Saves to login_subscription_ips (replace) + rebuilds login_allowed_ips

auth_service.py:
- Login check: only runs when LOGIN_ALLOWLIST_ENABLED is 'true'
- Combines subscription_ips + manual_ips at auth time (no stale combined key needed)

settings.py:
- POST /ip-allowlist/toggle: quick enable/disable endpoint
- GET /ip-allowlist: returns subscription_ips, manual_ips separately
- POST /ip-allowlist: saves manual_ips + optional subscription_url + optional enabled

settings.html:
- Toggle switch: enable/disable with warning if list is empty
- Green notice when enabled (warns about self-lockout risk)
- Grey notice when disabled
- Subscription IPs section: read-only, auto-refresh badge
- Manual IPs section: deletable per-IP, clear all button

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 18:19:47 +08:00
Your Name da43960f38 feat: subscription URL stored in settings, auto-refresh allowlist every 2h
config.py:
- LOGIN_SUBSCRIPTION_URL: stored in DB, configurable from UI
- LOGIN_MANUAL_IPS: manually added IPs (preserved across auto-refresh)

background/ip_allowlist_refresh.py (new):
- ip_allowlist_refresh_loop(): primary-worker task, every 2h
- _do_refresh(): fetch subscription, parse IPs, merge with manual IPs,
  write combined to LOGIN_ALLOWED_IPS + DB setting; runs once at startup (5s delay)
- get_last_refresh_time(): returns last successful refresh timestamp

main.py:
- Register ip_allowlist_refresh_loop as background task

settings.py:
- GET /ip-allowlist: now returns subscription_url, manual_ips, last_refresh
- POST /ip-allowlist: saves manual_ips + subscription_url, triggers refresh
- POST /ip-allowlist/manual: append to manual list only, trigger rebuild
- DELETE /ip-allowlist/ip: remove single manual IP, trigger rebuild

settings.html:
- Subscription URL field with 保存 + 🔄 instant-refresh button
- Last refresh timestamp display
- Status badge shows '订阅自动刷新' when URL configured
- IP badges: blue=manual (deletable), grey=from subscription (auto)
- 预览 button to inspect subscription IPs without saving

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 18:15:29 +08:00
Your Name 5fcc1e22a6 feat: login IP allowlist with proxy subscription parser
server/infrastructure/subscription_parser.py (new):
- parse_subscription_content(): fetch and decode subscription, extract host/IP
- Supports SS, VMess, VLESS, Trojan, Hysteria2 formats
- Base64 decode with padding fix; IPv4/CIDR/hostname detection

server/config.py:
- LOGIN_ALLOWED_IPS setting (empty = allow all, comma-separated)

server/api/settings.py:
- POST /settings/ip-allowlist/parse-subscription: fetch URL, decode, return hosts
- POST /settings/ip-allowlist: save allowlist to settings table + reload
- GET /settings/ip-allowlist: return current list

server/application/services/auth_service.py:
- _ip_in_allowlist(): supports exact IP, CIDR (ipaddress module), hostname match
- login(): check allowlist before brute-force check; if IP blocked return
  reason='ip_blocked' with clear message; audit log 'login_ip_blocked'

web/app/settings.html:
- New '登录 IP 白名单' card with:
  - Subscription URL input + parse button (calls backend parser)
  - Parsed IPs with checkboxes (select/deselect before adding)
  - Manual IP/CIDR/hostname textarea input
  - Current allowlist with per-IP remove buttons
  - Clear all button (removes restriction)
  - Status badge: 启用 N条 / 未启用(不限制)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 18:11:23 +08:00
Your Name 37ceaef6d9 feat: auto-poll Agent online status after install with live badge update
- remoteInstallAgent(): after success, start _startAgentOnlinePoller() instead
  of one-shot setTimeout(loadServers, 2000)
- _startAgentOnlinePoller(serverId): poll GET /api/servers/{id} every 5s up to
  2 minutes (24 attempts); when is_online=true, update status badge live,
  disable remote-install button, toast 'Agent 已上线', reload server list
- _updateAgentStatusBadge(isOnline): in-place badge update without full reload
- Install log shows: '✓ 安装成功!等待首次心跳...' then ' Agent 心跳已收到'
- Hint text explains: install ~30-60s, first heartbeat another ~60s
- Manual install: 'click to wait for Agent online' button triggers same poller

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:47:22 +08:00
Your Name d414b945e1 feat: audit log time-range filter + script execution schedules
Audit log time-range filter:
- audit_log_repo.py: unified query() with action/admin_username/date_from/date_to
  filters + total count; _build_filters() helper for date parsing
- settings.py GET /audit/: add date_from, date_to, admin_username query params
- audit.html: date range inputs (YYYY-MM-DD), admin filter input, action filter
  expanded with optgroups (服务器/推送/脚本/认证/设置); clear filter button

Script execution schedules:
- domain/models: PushSchedule extended with schedule_type('push'|'script'),
  script_id FK, script_content Text, exec_timeout int, long_task bool;
  source_path made nullable
- migrations.py: 6 new ALTER TABLE statements (idempotent)
- schedule_runner.py: detect schedule_type; for 'script' type use ScriptService;
  for 'push' type keep original SyncEngineV2 sync_files behaviour
- schemas.py: ScheduleCreate/Update add all new fields
- schedules.html: full UI rewrite
  - type selector radio: 📁 文件推送 /  脚本执行
  - script fields: script library dropdown + textarea + timeout + long_task
  - schedule list shows type badge + detail preview
  - saveSched() validates required fields per type

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:29:16 +08:00
Your Name 4ca07fa252 feat: quick execute panel in scripts.html (no script save required)
Add collapsible '直接执行命令' card at top of scripts page:
- Textarea for ad-hoc command (no script_id, not saved to library)
- Multi-select server list (loaded on first expand, same batch size)
- Options: timeout, DB credential (* substitution), long_task nohup
- Ctrl+Enter shortcut to submit
- Uses same batch tracking + status panel + polling as saved scripts
- Clear button to reset command
- Batch hint shows server count and batch size
- 全选 button for quick server selection

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:23:25 +08:00
Your Name d2832567c7 feat: push logs view in commands.html + paginated sync log API
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
sync_log_repo.py:
- get_all_paginated(): server_id/status/sync_mode/trigger_type filters +
  server name JOIN + total count for pagination

server/api/servers.py GET /servers/logs:
- Add server_id/status/sync_mode/trigger_type/offset/limit query params
- Return {items, total, offset, limit} instead of plain list

web/app/commands.html (renamed title to '操作日志'):
- Third view: '推送日志' with paginated table
- Columns: 状态(dot badge) / 服务器 / 模式 / 触发方式 / 源路径 /
  目标路径 / 文件数 / 耗时 / 操作人 / 开始时间
- Filters: 服务器 + 状态(success/failed/running) + 模式 (incremental/full/...)
- Pagination controls with prev/next page (50 per page)
- Error message in row title attribute (hover for details)

web/app/push.html:
- Update loadHistory() to handle new {items, total} response format

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:20:53 +08:00
Your Name 14dc29f241 feat: Agent install tab with copy command + one-click remote install via SSH
server/api/servers.py:
- POST /{id}/install-agent: SSH-execute install.sh on managed server using
  stored credentials; requires API_BASE_URL + agent_api_key; 120s timeout
- GET /{id}/agent-install-cmd: return pre-filled curl command for copy-paste

web/app/servers.html:
- New 'Agent 安装' tab in server detail panel
  - Shows Agent online/offline status badge
  - Warns if NEXUS_API_BASE_URL or API Key is missing
  - Displays ready-to-copy curl install command with port auto-filled
  - ' 一键远程安装' button (SSH exec): disabled if already online
  - Scrollable install log output panel
  - Tab uses shared showDetailTab() + loadAgentInstall() / remoteInstallAgent()
- Explains: script auto-installs Python, downloads agent.py, writes config,
  sets up systemd, opens firewall port

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:16:14 +08:00
Your Name b9e8db7a3f feat: per-alert notification toggles with switch UI
config.py:
- Add NOTIFY_ALERT_CPU/MEM/DISK, NOTIFY_RECOVERY, NOTIFY_TIME_DRIFT,
  NOTIFY_SYSTEM_REDIS, NOTIFY_SYSTEM_MYSQL, NOTIFY_RESTART settings
  (string 'true'/'false', loaded from MySQL settings table at startup)

telegram/__init__.py:
- _notify_enabled(): check setting string ('false'/'0'/'no'/'off' = disabled)
- send_telegram_alert(): checks NOTIFY_ALERT_{CPU|MEM|DISK} per alert_type
- send_telegram_recovery(): checks NOTIFY_RECOVERY
- send_telegram_system_alert(): new notify_key param, checks matching setting
- send_telegram_restart_*(): check NOTIFY_RESTART

self_monitor.py:
- Pass notify_key='NOTIFY_SYSTEM_REDIS/MYSQL' to send_telegram_system_alert

server/api/agent.py:
- Pass notify_key='NOTIFY_TIME_DRIFT' to time-drift alert

settings.html:
- New '告警通知项目' section with 8 toggle switches (pill style)
- Grouped by category: 服务器资源 / 服务器 / 系统
- Optimistic toggle with revert on API failure
- renderNotifyToggles() reads _allSettings and toggleNotify() saves instantly

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:12:07 +08:00
Your Name 08d0baf762 feat: TOTP re-bind support + improved settings UI
settings.html TOTP section overhaul:
- Status cards: green (enabled) / amber (disabled) with context-aware buttons
- Enabled state: add '重新绑定' button to change Authenticator app/device
  without disabling first (same API, just new QR + verify flow)
- Disabled state: prominent 'Enable TOTP' CTA with warning about security
- Bind panel: shared between first-time setup and re-bind, auto-scrolls into view
- Auto-focus verify input after QR renders; Enter key submits
- QR generation: 180px, transparent background, fallback message if QRious fails
- Re-bind label distinguishes 'rebind success' from 'first enable' toast
- Disable flow: clearer confirm dialog mentioning security implication

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:07:40 +08:00
Your Name 9c063a788f feat: time drift detection between agent and central server
Agent (web/agent/agent.py):
- Heartbeat now includes system_info.agent_time (UTC ISO string)

Central (server/api/agent.py):
- On each heartbeat, compare agent_time vs central datetime.now(utc)
- Thresholds: warn >= 30s, crit >= 60s (affects TOTP/cron accuracy)
- Stores time_drift_seconds + drift_level in Redis heartbeat key
- Sends Telegram alert (5-min cooldown) when drift >= 60s critical

API (server/api/servers.py):
- List and detail endpoints expose time_drift_seconds + drift_level from Redis

Frontend (web/app/servers.html):
- driftBadge(): shows clock emoji badge next to server name
  - amber badge: >=30s (warn)
  - red badge: >=60s (crit)
  - tooltip shows exact drift and NTP check hint
  - no badge when drift < 30s or no data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:04:34 +08:00
Your Name 5a42345bc0 docs: Nexus 6.0 功能说明书 HTML + roadmap stale fix
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:55:24 +08:00
Your Name 6c5e6a2daa feat: file operations (delete/rename/mkdir) + fix roadmap Step3 status
Backend:
- schemas.py: FileOperation model (server_id, operation, path, new_path)
- sync_v2.py: POST /api/sync/file-ops endpoint
  - delete: rm -rf with shlex.quote
  - rename: mv src -> dst with shlex.quote on both paths
  - mkdir: mkdir -p with shlex.quote
  - all operations audit-logged, 30s timeout

Frontend (files.html):
- Header: add 📁+ button (doMkdir via prompt)
- File rows: action buttons (rename + delete) appear on hover
  - rename: prompt for new name, inline mv
  - delete: confirm dialog with warning for dirs (rm -rf)
  - all ops refresh current directory after success
- fileOp() shared helper for POST /api/sync/file-ops

Docs:
- roadmap.md: Step 3 Web SSH updated from 70% to completed

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:46:00 +08:00
Your Name ac0eb79707 feat: rsync dry-run preview (方案D 智能提示型)
Backend:
- schemas.py: SyncPreview model (server_id, source/target_path, sync_mode, verbose)
- sync_engine_v2.py: preview_sync() + _parse_rsync_stats() helpers
  - runs rsync --dry-run --stats; verbose=True adds -v (capped 200 lines)
  - returns stats dict: files_transferred/created/deleted + byte counts
- sync_v2.py: POST /api/sync/preview endpoint with audit log

Frontend (push.html - 方案D):
- Sync mode radio change listener: onSyncModeChange()
- full/overwrite mode: orange warning card + prominent preview button
- incremental/checksum mode: subtle "预览首台变动 →" link
- Inline preview result section (no modal): stats cards + optional file list
- files_deleted shown in red card for destructive modes
- verbose toggle: "显示详细文件列表 ▼" triggers second request with verbose=true
- fmtBytes() helper for human-readable file sizes

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:41:47 +08:00
Your Name e37bc6cea9 docs: drop file editor (Monaco/ACE) - officially rejected
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:31:33 +08:00
Your Name 2c6f3abbd0 docs: add standards/ folder with all reusable dev & audit standards
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:23:50 +08:00
Your Name d5c498549b docs: system development standard v1.0 - full coding rules for Python/FastAPI/Frontend
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:22:36 +08:00
Your Name 07eab6020c docs: extract audit core principles as standalone reference card
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:19:49 +08:00
Your Name 938e700fb3 docs: line-walk audit standard v2.0 - full spec integrating mdc rules + Phase 1-4 lessons
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:17:27 +08:00
Your Name 424c4efd64 docs: general-purpose line-walk audit standard (reusable for any project/AI)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:09:28 +08:00
Your Name c08e2a2242 fix: Phase 1 audit - ssh_key columns String(500) -> Text for RSA key support
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:07:24 +08:00
Your Name 6108d69b24 fix: Phase 4 audit fixes
F4a-01 script_callback_rate: INCR+EXPIRE were non-atomic; a crash between
       the two could leave the rate-limit key without a TTL, permanently
       blocking future callbacks for that job/IP. Fix: use Redis pipeline
       so INCR+EXPIRE are sent in one roundtrip and EXPIRE always executes.

F4a-02 sync_service: add comment documenting that StrictHostKeyChecking=no
       is a legacy issue in the dead-code SyncService shim; main sync path
       (SyncEngineV2) honours SSH_STRICT_HOST_CHECKING from settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:00:28 +08:00
Your Name 680577fc97 docs: Phase 3 walk-closure report + index update
- docs/reports/audit-phase-3-walk-closure.md: closure declaration
  covering 3a-3i (12 new FINDINGs, all fixed, code backlog = 0)
- docs/reports/README.md: add Phase 3 closure entry

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:56:11 +08:00
Your Name 86bb32f87e fix: Phase 3h script_service + server_service
F3h-01 script_service._build_kill_command: shlex.quote(log_path) wraps the
       path in single-quotes; embedding that inside "..." produced a filename
       with literal single-quote chars that cat/kill could not find.
       Fix: use the shlex-quoted string directly without extra double-quotes.

F3h-02 script_service._substitute_db_vars: non-atomic string replacement
       allowed nested substitution when a credential value contained another
       $DB_* pattern. Fix: replace with null-byte sentinels first, then
       substitute actual values in a second pass.

server_service.push_to_servers: add comment documenting that audit_repo and
       retry_repo are None (internal compat shim — not used by routes).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:50:44 +08:00
Your Name c3c3bae370 fix: Phase 3g sync_engine_v2 bugs
F3g-01 sync_commands line 195: server_results is a list; unpacking it
       with ** raises TypeError, silently caught by gather(return_exceptions=True),
       so S2 command sync and S3 config sync returned empty results for all
       servers. Fixed by using a proper dict: {"server_name": ..., "results": ...}.

F3g-02 sync_config: config value containing newline chars was not stripped
       before shlex.quote; echo inside single-quotes would write multiple lines
       to the sysctl config file, injecting arbitrary keys. Strip \n\r\x00 from
       value before quoting (requires admin auth, defense-in-depth fix).

scripts.py, settings.py: CLEAN (no changes needed).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:46:52 +08:00
Your Name 33b7ed93bf fix: Phase 3e auth_service.py bugs
F3e-01 auth_service.py:180 - jwt_token_expires (MySQL tz-naive) vs
       datetime.now(timezone.utc) caused TypeError on token refresh;
       strip tzinfo before comparison when expires is tz-naive.

F3e-02 auth_service.py:271 - TOTP provisioning URI was not URL-encoding
       issuer or username; special chars (spaces etc.) broke QR scan.
       Use urllib.parse.quote() on both fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:41:50 +08:00
Your Name 61b891db73 fix: Phase 3 line-walk fixes (3a-3d)
F3a-01 agent.py: _safe_float() prevents ValueError from non-numeric metric values
F3b-01 schedule_runner.py: fix tz-aware minus naive DateTime TypeError
F3b-02 schedule_runner.py: cron field divided by 0 returns False instead of crash
F3c-01 redis/client.py: mask credentials in REDIS_URL log output
F3d-01 main.py: cancel background tasks when primary worker lock is lost
F3d-02 main.py: strip whitespace from CORS origins after split

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:37:15 +08:00
Your Name aa65f10c52 docs: Phase 2 audit records, project memory, WSL guide
Audit docs:
- docs/reports/audit-phase-2{a-j}-line-walk.md: per-file line walk findings
- docs/reports/audit-phase-2-findings-matrix.md: 63 findings (52 fixed, 3 partial, 7 accepted)
- docs/reports/2026-05-23-full-audit-report.md: full audit consolidation
- docs/reports/audit-full-vs-phase2-reconciliation.md: cross-reference
- docs/reports/audit-phase-2-walk-closure.md: closure sign-off

Project docs:
- docs/project/phase-2-audit-remediation.md: SSOT for all fixes
- docs/project/alert-push-policy.md: dashboard alert design decisions
- docs/project/production-verification-checklist.md: first-deploy checklist
- docs/project/script-execution.md: script platform design
- docs/project/wsl-integration-test.md: WSL code-only verification guide
- docs/project/line-walk-audit-standard.md: audit methodology
- docs/project/mysql-mcp-setup.md: MCP config guide

Changelogs: 18 entries (2026-05-22 to 2026-05-23)
Project memory: CLAUDE.md, AGENTS.md updated to reflect current state
Deploy: nginx_https.conf template updated

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:27:40 +08:00
Your Name 80d73d1b74 fix: WSL startup compatibility + dependency updates
- redis/client.py: use BlockingConnectionPool.from_url() (fixes 'url' kwarg error)
- database/engine_compat.py: patch aiomysql do_ping to satisfy pool_pre_ping
- database/session.py: apply engine_compat patch before create_async_engine
- requirements.txt: SQLAlchemy 2.0.49 (fixes aiomysql ping() reconnect signature)
- .env.example: document NEXUS_REDIS_URL format
- scripts/wsl_ensure_venv.sh: create project .venv (PEP 668 safe)
- scripts/wsl_start_dev.sh: use .venv python, validate Redis before start
- scripts/wsl_integration_smoke.sh: code-only verification mode
- scripts/wsl_check_mysql.py / bootstrap_database.py: MySQL setup helpers
- scripts/sync_frontend_vendor.py: vendor asset sync utility

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:27:18 +08:00
Your Name 752a24497c feat: Phase 2 security audit fixes + script execution platform
Security (P0/P1/P2):
- WebSSH: dedicated short-lived token, server_id binding, 4003 close code
- Remove /api/agent/exec from control plane (RCE surface eliminated)
- Global JWT middleware (JwtAuthMiddleware) with install-mode bypass
- decrypt_value: failure returns None, never leaks ciphertext
- Telegram: sanitize_external_message strips sensitive fields
- Agent auth: startup enforces non-empty API key, compare_digest
- Credentials: password_set bool flag, no plaintext in API responses
- audit_log: writes admin_username + ip_address on all CUD ops
- Install lock: all /api/install/* except GET /status return 403 post-install
- WebSocket dedup: publish once, subscribers deliver locally

Script execution platform:
- script_jobs.py / script_job_callback.py: async batching and callback
- script_execution_store.py / script_callback_rate.py: Redis-backed state
- script_execution_flush.py: background flusher to MySQL
- scripts.html / script-executions.html: full execution UI
- agent_url.py: centralised URL builder

Frontend:
- All 13 pages migrated from CDN to /app/vendor/ (no external deps)
- vendor/: alpinejs, tailwindcss-browser, xterm, xterm-addon-*, qrious
- Dashboard WebSocket real-time alerts; 8h JWT session timeout

Tests:
- test_security_unit.py: 15 unit tests (JWT, sanitize, vendor, install 403)
- test_api.py: env-configurable admin credentials

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:26:56 +08:00
Your Name 341d16fd6d P2迭代: 危险命令检测 + DB会话合并 + API密钥修复 + 安全审计 (6项)
P2-14: agent.py _verify_api_key() 重写文档并明确两步验证逻辑,
       全局密钥匹配直接通过,非匹配密钥放行至handler做per-server验证
P2-15: websocket.py 删除已废弃的 connected_clients=[] 别名,
       health.py 已使用 manager.client_count
P2-16: webssh.py 合并4个AsyncSessionLocal()为1个共享session(预接受操作),
       命令日志保留独立session(长连接WebSocket不能持有单session)
P2-18: 新增危险命令检测 (check_dangerous_command),识别 rm -rf /、
       fork bomb、dd写裸设备、mkfs等模式,scripts.py + webssh.py集成
P2-19: install.py 状态文件不再存储db_pass明文(步骤5已删除文件),
       审计确认所有logger调用无敏感数据泄露
P2-17: httpOnly cookie迁移标记为延期(需前后端+WebSocket全面重构,
       当前JWT+updated_at+CORS限制方案已足够安全)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:18:08 +08:00
Your Name d744d7df8e 安全与质量修复: P0-4/P0-5 + P1-6~P1-13 (8项)
P0-4: install.py _write_env() 值加引号转义,防止含#$\等字符的密码破坏.env格式
P0-5: install.py _build_redis_url() URL编码redis密码,防止@:等字符破坏URL解析
P1-6: auth_service.py _decode_access_token() 验证exp/sub声明存在,拒绝畸形JWT
P1-7: websocket.py + webssh.py WebSocket JWT验证增加updated_at检查,密码修改后令牌失效
P1-8: auth.py 无refresh_token的logout返回更明确的提示信息
P1-9: install.py create_admin增加_is_installed()检查,防止.env存在后重复创建
P1-10: servers.py server_stats() 改用SQL聚合查询,避免加载全部服务器对象
P1-11: heartbeat_flush.py 移除get_redis()永不返回None的死代码
P1-13: sync_engine_v2.py completed/failed计数器加asyncio.Lock防止并发竞态

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:04:40 +08:00
Your Name 4af9acf564 项目文档与验证脚本: AGENTS.md + 交接文档 + 审计/测试脚本
- AGENTS.md: 项目记忆(与CLAUDE.md同步)
- PROJECT_HANDOVER.md: 项目交接文档
- TEAM_ROLES.md: 团队分工与职责
- audit_scan.py: 全量代码审计扫描
- test_p1_p2.py: P1+P2功能快速验证
- verify.py: WSL模块/路由/安全验证

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:29:54 +08:00
Your Name 6354d507df 修复 sync_v2.py 缺少 AuditLog import (NameError)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:29:35 +08:00
Your Name 7e29397235 配置与文档更新: .gitignore + CLAUDE.md + API微调
- .gitignore: 添加.env/supervisor/日志等忽略规则
- CLAUDE.md: 更新项目记忆与完成状态
- assets.py: Platform/Node API端点
- settings.py: DB_OVERRIDE_MAP键名修复
- schemas.py: 模型schema补充

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:29:20 +08:00
Your Name 4969876ce8 新页面: 资产管理 + 命令日志 + 数据库备份脚本
- assets.html: Platform/Node资产管理页面
- commands.html: 命令/会话双视图 + 危险命令高亮
- deploy/db_backup.sh: mysqldump + 30天保留 + cron集成

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:29:13 +08:00
Your Name 8c8d2a74d5 docs: add Vercel-style documentation browser
Tailwind CSS + Alpine.js single-page HTML doc viewer with:
- 274 documents embedded (270 md + 4 other file types)
- Dark/light mode with localStorage persistence
- Sidebar navigation with grouped collapsible sections
- Full-text search (filename + content)
- Auto-generated TOC for each document
- Terminal-style code blocks with language labels
- Mobile responsive with slide-out sidebar
- rebuild: python docs/build_html.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 07:26:31 +08:00
11947 changed files with 478408 additions and 65075 deletions
-69
View File
@@ -1,69 +0,0 @@
{
"permissions": {
"allow": [
"Bash(git checkout *)",
"Bash(python -m py_compile server/database.py server/services/retry_engine.py server/services/sync_engine.py server/api/servers.py server/main.py)",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(python -m py_compile server/api/servers.py)",
"Bash(python -m py_compile agent/agent.py)",
"Bash(python -m py_compile server/main.py server/api/servers.py)",
"Bash(python -m py_compile server/main.py)",
"Bash(python -m py_compile server/config.py server/api/servers.py server/services/retry_engine.py server/services/notify.py)",
"Bash(python -m py_compile server/config.py)",
"Bash(python -m py_compile server/database.py)",
"Bash(python -m py_compile server/database.py server/services/scheduler.py server/api/servers.py server/main.py)",
"Bash(python -m py_compile server/services/retry_engine.py)",
"Bash(python -m py_compile server/config.py server/main.py server/api/servers.py)",
"Bash(python -m py_compile agent/agent.py server/api/servers.py)",
"Bash(python -m py_compile server/services/sync_engine.py server/services/retry_engine.py server/api/servers.py)",
"Bash(xargs python *)",
"Bash(echo \"EXIT:$?\")",
"Bash(python -m py_compile server/services/sync_engine.py agent/agent.py)",
"Bash(python -m py_compile server/api/auth.py server/services/retry_engine.py)",
"Bash(python -m py_compile server/main.py server/database.py)",
"Bash(python -m py_compile server/config.py server/main.py)",
"Bash(python -m py_compile server/services/monitor.py server/main.py)",
"Bash(python -m py_compile server/services/monitor.py)",
"WebFetch(domain:github.com)",
"Bash(sshpass -p 'root' ssh-copy-id -o StrictHostKeyChecking=no root@192.168.124.10)",
"Bash(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@192.168.124.10 \"hostname && uname -a\")",
"Bash(apt-get install *)",
"Bash(yum install *)",
"Read(//c/Users/uzuma/.ssh/**)",
"Bash(python -c ' *)",
"Bash(ssh -o ConnectTimeout=5 root@192.168.124.10)",
"Bash(awk '{print $2}')",
"WebFetch(domain:47.76.187.108)",
"WebFetch(domain:api.synaglobal.vip)",
"Bash(pip install *)",
"Bash(where ssh *)",
"Bash(ssh -V)",
"Bash(cp \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\.mcp.json\" \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.mcp.json\")",
"Bash(cp \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\mcp\\\\mcp_bridge.py\" \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\mcp\\\\mcp_bridge.py\")",
"Read(//c/Users/uzuma/Desktop/svn/multi-server-sync/**)",
"Read(//c/Users/uzuma/.claude/**)",
"WebSearch",
"Bash(cmd /c *)",
"Bash(echo \"EXIT: $?\")",
"Bash(ssh -o StrictHostKeyChecking=no -i /c/Users/uzuma/.ssh/id_rsa_mcp root@47.76.187.108 \"python3 /www/wwwroot/api.synaglobal.vip/mcp/multisync_server.py\")",
"Bash(scp -o StrictHostKeyChecking=no -i /c/Users/uzuma/.ssh/id_rsa_mcp \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\mcp\\\\multisync_server.py\" root@47.76.187.108:/www/wwwroot/api.synaglobal.vip/mcp/multisync_server.py)",
"Bash(git push *)",
"Bash(claude mcp *)",
"Skill(update-config)",
"Skill(update-config:*)",
"Bash(python -c \"import json; d=json.load\\(open\\(r'C:\\\\Users\\\\uzuma\\\\.claude.json'\\)\\); print\\(json.dumps\\(d.get\\('mcpServers',{}\\), indent=2\\)\\)\")",
"mcp__multisync__service",
"mcp__multisync__api_call",
"mcp__multisync__git_log",
"mcp__multisync__deploy",
"mcp__multisync__config_view",
"mcp__multisync__db_query",
"mcp__multisync__logs",
"mcp__multisync__run_test",
"Bash(python -m py_compile \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\server\\\\api\\\\servers.py\")",
"Bash(python -m py_compile \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\agent\\\\agent.py\")",
"Bash(python -m py_compile \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\server\\\\services\\\\agent_installer.py\")"
]
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"mcpServers": {
"mysql-mcp": {
"command": "bash",
"args": [
"scripts/linux_mcp_mysql.sh"
],
"env": {}
},
"gitea-mcp": {
"command": "bash",
"args": [
"scripts/mcp_gitea.sh"
],
"env": {
"GITEA_HOST": "https://axs.kuma1xn.vip",
"GITEA_ACCESS_TOKEN_FILE": "/mnt/c/Users/uzuma/.codex/secrets/gitea-axs.token",
"HTTPS_PROXY": "socks5://127.0.0.1:10808",
"HTTP_PROXY": "socks5://127.0.0.1:10808"
}
}
}
}
@@ -0,0 +1,608 @@
---
name: Nexus 全面代码审计
overview: 对 Nexus 6.0 worktree 进行只读全面扫描,按 8 类问题输出带位置、严重度与修复建议的审计报告;WSL Node20/MCP 本地权限任务已交付,不在本次重复实施。
todos:
- id: fix-p0-webssh
content: "P0: WebSSH 强制 JWT server_id 绑定 + 失败时 close WebSocket"
status: completed
- id: fix-p1-xss
content: "P1: servers/scripts 等 innerHTML 字段统一 esc()"
status: pending
- id: fix-p1-agent-apikey
content: "P1: 收敛 Agent exec + API Key reveal + compare_digest"
status: pending
- id: fix-p2-heartbeat-stats
content: "P2: 心跳仅 Redis + 在线统计与列表数据源一致"
status: pending
- id: fix-p2-hardening
content: "P2: install 路由锁定、TOTP disable 二次验证、全局 JWT 中间件"
status: pending
- id: add-security-tests
content: 补充 auth/webssh/agent 集成测试与 pip-audit CI
status: pending
isProject: false
---
# Nexus 6.0 全面代码审计报告
**范围**: [`server/`](server/)、[`web/app/`](web/app/)、[`scripts/`](scripts/)、[`deploy/`](deploy/)、[`requirements.txt`](requirements.txt)
**模式**: 只读扫描(未改代码)
**说明**: 你反复收到的「WSL Node 20 + 本地库最大权限」计划**已在 worktree 中实现并验证**Node v20.19.2、MYSQL_READONLY=false、GRANT ALL、MCP Connected)。请勿再重复执行该计划;下文为**新的**全项目审计。
**严重度**: P0 致命 / P1 高 / P2 中 / P3 低 / Info 优化建议
**审计统计(地毯式,2026-05-22**
| 类别 | 文件数 | 状态 |
|------|--------|------|
| `server/` Python | 55 | 55/55 已逐文件读/扫 |
| `web/` 前端+Agent | 23 | 23/23 已逐文件读/扫 |
| `scripts/` | 8 | 8/8 已审计 |
| `deploy/` | 6 | 6/6 已审计 |
| `tests/` + 根目录测试 | 8 | 8/8 已审计 |
| 根配置/CI/MCP | 5 | 5/5 已审计 |
| `docs/` 文档(非可执行) | ~350 | 密钥模式扫描,无新增硬编码密钥 |
**P0 共 4 项** | **P1 共 18 项**(含 sysctl 注入补遗)
---
## 0. P0 / P1 问题总表(复查用)
### P04
| ID | 文件 | 问题 |
|----|------|------|
| S-01 | `server/api/webssh.py` | 标准 JWT 无 `server_id` 时可 IDOR 连任意服务器 SSH |
| S-02 | `server/api/agent.py` | `/api/agent/exec` + `subprocess_shell` + 全局 API Key = 中枢 RCE |
| S-13 | `tests/load_test.py`, `tests/quick_load.py` | **仓库内硬编码 `msk-ufkvjm8...` API Key**(若生产密钥则灾难性泄露) |
| S-14 | `web/agent/agent.py` | 子机 Agent `/exec` 同样 shell 执行;`verify_api_key``!=` 非常量时间 |
### P118,节选)
| ID | 文件 | 问题 |
|----|------|------|
| **S-19** | `server/application/services/sync_engine_v2.py` L234-236 | **sysctl/echo 命令注入**`sysctl -w {key}={safe_value}` 仅 quote 值,含 `'` 可打断 shell |
| **S-20** | `server/application/services/sync_engine_v2.py` L281-284 | **rollback 命令注入**`backup_file` 未 quote 拼入 `cp ... && sysctl --system` |
| S-03 | `server/api/settings.py` | `/api-key/reveal` 回读全局 API_KEY |
| S-04 | `web/app/servers.html` | `category`/`agent_version` 未 esc → XSS |
| S-05 | `web/app/scripts.html` | `category` 未 esc → XSS |
| S-06 | `web/app/api.js` | JWT 存 localStorage |
| S-07 | `web/app/index.html`, `terminal.html` | WS JWT 在 URL |
| S-08 | `server/api/install.py` | 生产仍暴露无认证 install API |
| S-09 | `server/api/auth.py` | TOTP disable 无二次验证 |
| S-15 | `server/api/agent.py` | 全局 Key 可伪造任意无 per-key 服务器的心跳 |
| S-16 | `web/install.php` | 遗留 PHP 安装面(与 install.html 双轨) |
| S-17 | `server/main.py` + FastAPI | `/docs` `/openapi.json` 未鉴权暴露 API 结构 |
| R-01 | `server/infrastructure/database/crypto.py` | AES-CBC 遗留 + 解密失败返回密文 |
| E-01 | `server/main.py` | 无全局 JWT 中间件,仅靠各路由 Depends |
| P-01 | `server/api/agent.py` | 心跳直写 MySQL |
| B-01 | 同 S-01 | WebSSH IDOR(功能+安全双重) |
| S-18 | `server/application/services/script_service.py` | 调用 Agent 用 `http://` 明文,MITM 可篡改命令 |
---
## 9. 逐文件审计打勾(可执行代码 99 个)
### server/55
- [x] `server/__init__.py` 已审计 ✓ — 无问题
- [x] `server/main.py` 已审计 ✓ — E-01 无全局 JWTinstall 路由常开;InstallMode 放行 `/ws/`SecurityHeaders 无 CSP
- [x] `server/config.py` 已审计 ✓ — R-02 默认 `root:password` DATABASE_URL
- [x] `server/api/__init__.py` 已审计 ✓
- [x] `server/api/agent.py` 已审计 ✓ — **P0 S-02**P-01S-15S-12 timing
- [x] `server/api/assets.py` 已审计 ✓ — JWT 保护完整
- [x] `server/api/auth.py` 已审计 ✓ — S-09 totp/disable
- [x] `server/api/auth_jwt.py` 已审计 ✓ — PUBLIC_PREFIXES 未作全局中间件;逻辑正确
- [x] `server/api/dependencies.py` 已审计 ✓ — S-10 危险命令仅警告
- [x] `server/api/health.py` 已审计 ✓ — 公开合理
- [x] `server/api/install.py` 已审计 ✓ — **S-08**C-03 阻塞 subprocesslock 无 JWT
- [x] `server/api/schemas.py` 已审计 ✓ — Pydantic 校验良好
- [x] `server/api/scripts.py` 已审计 ✓ — JWT;凭据脱敏需对照响应
- [x] `server/api/search.py` 已审计 ✓ — LIKE 转义
- [x] `server/api/servers.py` 已审计 ✓ — B-03/P-02agent-key 返回明文 key(设计如此,需审计日志)
- [x] `server/api/settings.py` 已审计 ✓ — **S-03** reveal
- [x] `server/api/sync_v2.py` 已审计 ✓ — browse 用 shlex.quote
- [x] `server/api/webssh.py` 已审计 ✓ — **P0 S-01**B-02 未 close
- [x] `server/api/websocket.py` 已审计 ✓ — S-07P-03 多 worker 去重
- [x] `server/application/__init__.py` 已审计 ✓
- [x] `server/application/services/__init__.py` 已审计 ✓
- [x] `server/application/services/auth_service.py` 已审计 ✓ — R-07 旧 refresh
- [x] `server/application/services/script_service.py` 已审计 ✓ — S-18 HTTPtest_cmd 含密码若暴露 API 则 P1
- [x] `server/application/services/server_service.py` 已审计 ✓
- [x] `server/application/services/sync_engine_v2.py` 已审计 ✓ — **P1 S-19/S-20 sysctl 命令注入**(初版漏报);rsync 用 shlex
- [x] `server/application/services/sync_service.py` 已审计 ✓
- [x] `server/background/__init__.py` 已审计 ✓
- [x] `server/background/heartbeat_flush.py` 已审计 ✓ — 与 B-04 应对齐
- [x] `server/background/retry_runner.py` 已审计 ✓
- [x] `server/background/schedule_runner.py` 已审计 ✓
- [x] `server/background/self_monitor.py` 已审计 ✓
- [x] `server/domain/__init__.py` 已审计 ✓
- [x] `server/domain/models/__init__.py` 已审计 ✓ — 敏感列设计合理
- [x] `server/domain/repositories/__init__.py` 已审计 ✓
- [x] `server/infrastructure/__init__.py` 已审计 ✓
- [x] `server/infrastructure/database/__init__.py` 已审计 ✓
- [x] `server/infrastructure/database/admin_repo.py` 已审计 ✓ — 登录限速仅 username+ip
- [x] `server/infrastructure/database/audit_log_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/crypto.py` 已审计 ✓ — R-01B-07
- [x] `server/infrastructure/database/db_credential_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/migrations.py` 已审计 ✓
- [x] `server/infrastructure/database/password_preset_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/platform_node_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/push_schedule_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/script_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/server_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/session.py` 已审计 ✓
- [x] `server/infrastructure/database/setting_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/ssh_session_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/sync_log_repo.py` 已审计 ✓
- [x] `server/infrastructure/redis/__init__.py` 已审计 ✓
- [x] `server/infrastructure/redis/client.py` 已审计 ✓
- [x] `server/infrastructure/ssh/__init__.py` 已审计 ✓
- [x] `server/infrastructure/ssh/asyncssh_pool.py` 已审计 ✓ — Info 压测
- [x] `server/infrastructure/telegram/__init__.py` 已审计 ✓
### web/23
- [x] `web/app/api.js` 已审计 ✓ — S-06U-01
- [x] `web/app/layout.js` 已审计 ✓ — 搜索 esc 良好
- [x] `web/app/login.html` 已审计 ✓ — S-06CDN 无 SRI
- [x] `web/app/index.html` 已审计 ✓ — S-07
- [x] `web/app/servers.html` 已审计 ✓ — **S-04**
- [x] `web/app/scripts.html` 已审计 ✓ — **S-05**
- [x] `web/app/terminal.html` 已审计 ✓ — S-07
- [x] `web/app/settings.html` 已审计 ✓ — U-03
- [x] `web/app/install.html` 已审计 ✓ — S-08 无认证 install fetch
- [x] `web/app/files.html` 已审计 ✓ — esc 一致
- [x] `web/app/push.html` 已审计 ✓ — esc 良好
- [x] `web/app/audit.html` 已审计 ✓
- [x] `web/app/assets.html` 已审计 ✓
- [x] `web/app/commands.html` 已审计 ✓ — S-11 onclick
- [x] `web/app/credentials.html` 已审计 ✓ — esc 良好
- [x] `web/app/schedules.html` 已审计 ✓
- [x] `web/app/retries.html` 已审计 ✓ — esc 良好
- [x] `web/favicon.svg` 已审计 ✓
- [x] `web/install.php` 已审计 ✓ — **S-16** 遗留安装
- [x] `web/agent/agent.py` 已审计 ✓ — **P0 S-14**
- [x] `web/agent/agent.sh` 已审计 ✓
- [x] `web/agent/install.sh` 已审计 ✓ — API key 进 config
- [x] `web/agent/config.example.json` 已审计 ✓
### scripts/8deploy/6tests+根(8)配置(5
- [x] `scripts/*.py/sh/sql/ps1` 8 文件 已审计 ✓ — R-05 R-06
- [x] `deploy/*` 6 文件 已审计 ✓ — R-04 nginx 无 CSP
- [x] `tests/*` + `test_p1_p2.py` 8 文件 已审计 ✓ — **P0 S-13** 硬编码密钥
- [x] `requirements.txt` `.env.example` `.gitea/workflows/ci-cd.yml` `.cursor/mcp.json` `PROJECT_HANDOVER.md` 已审计 ✓ — R-08CI 无 pip-audit
### docs/~350,批量)
- [x] `docs/**` 已审计 ✓ — 全文 rg 密钥模式,无新增 `msk-` 类硬编码(与 tests 除外)
---
## 1. 功能性缺陷(Bug
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| B-01 | P1 | [`server/api/webssh.py`](server/api/webssh.py) L94-97, L110-113 | JWT **无** `server_id` claim 时不校验路径 `server_id`,任意已登录管理员可改 URL 连任意服务器 SSHIDOR | 签发 WebSSH 专用短期 JWT(必含 `server_id`);或 `token_server_id is None` 时直接 `close(4003)` |
| B-02 | P1 | [`server/api/webssh.py`](server/api/webssh.py) L110-117 | 服务器不存在或凭据缺失时 `return``websocket.close()`,客户端挂起超时 | 统一 `accept``close(code, reason)` |
| B-03 | P2 | [`server/api/servers.py`](server/api/servers.py) L99-114 vs L58-63 | 列表用 Redis 心跳覆盖 `is_online``server_stats` 仍读 MySQL `is_online`,仪表盘与列表不一致 | 统计接口与列表共用 Redis 或统一写入策略 |
| B-04 | P2 | [`server/api/agent.py`](server/api/agent.py) L216-217 | 每次 Agent 心跳 `update_heartbeat` + `commit()`,与文档「Redis 实时 + 10min 批量落 MySQL」矛盾 | 心跳仅写 Redis,由 [`heartbeat_flush.py`](server/background/heartbeat_flush.py) 批量刷库 |
| B-05 | P2 | [`server/api/agent.py`](server/api/agent.py) L259-260 | `TimeoutError``proc.kill()``await proc.wait()`,可能僵尸进程 | `kill()` + `await communicate()``asyncio.create_task` 清理 |
| B-06 | P3 | [`web/app/api.js`](web/app/api.js) L97-101 | `sendBeacon` 登出可能不带 Bearer,后端若要求认证则注销无效 | 对齐后端 logout 契约或改用 `fetch` + `keepalive` |
| B-07 | P3 | [`server/infrastructure/database/crypto.py`](server/infrastructure/database/crypto.py) L75-83 | 解密失败返回密文字符串,SSH 可能用错误密码连接且难排查 | 解密失败抛 `DecryptionError`,API 返回明确 5xx/配置错误 |
---
## 2. 安全漏洞
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| S-01 | P0 | [`server/api/webssh.py`](server/api/webssh.py) + [`web/app/terminal.html`](web/app/terminal.html) L82-83 | WebSSH 横向越权(见 B-01+ JWT 在 URL query | 见 B-01WS 用首帧 auth 或短期 ticket,禁止 query token |
| S-02 | P0 | [`server/api/agent.py`](server/api/agent.py) L222-246 | `/api/agent/exec``create_subprocess_shell` 执行任意命令,仅全局 API Key | 命令白名单/参数化 `execve`per-server key;审计 + 二次确认 |
| S-03 | P1 | [`server/api/settings.py`](server/api/settings.py) L60-67 | `POST /api-key/reveal` 对已登录管理员返回完整全局 API_KEY | 禁止回读;仅轮换时展示一次;或 per-server key |
| S-04 | P1 | [`web/app/servers.html`](web/app/servers.html) L161-162 | `category``agent_version``esc()` 写入 `innerHTML` → 存储型 XSS | 所有 API 字符串字段统一 `esc()` 或 DOM API |
| S-05 | P1 | [`web/app/scripts.html`](web/app/scripts.html) L36 附近 | `category` 列表渲染未转义 | 同 S-04 |
| S-06 | P1 | [`web/app/api.js`](web/app/api.js) + [`login.html`](web/app/login.html) | JWT 存 `localStorage`XSS 即可窃取 refresh token | HttpOnly Cookie + CSRF;或严格 CSP + 消除 XSS |
| S-07 | P1 | [`web/app/index.html`](web/app/index.html) L208 | `/ws/alerts?token=` JWT 进 URL/日志/Referer | 同 S-01 WS 鉴权改造 |
| S-08 | P1 | [`server/api/install.py`](server/api/install.py) | 生产仍挂载 install 路由;`/lock``/status` 等无 JWT | 安装完成后卸载路由或 IP 白名单 + 一次性 token |
| S-09 | P1 | [`server/api/auth.py`](server/api/auth.py) L151-165 | `totp/disable` 仅需 JWT,无密码/TOTP 二次验证 | 要求 `current_password` + 有效 TOTP |
| S-10 | P2 | [`server/api/dependencies.py`](server/api/dependencies.py) L167-172 | 危险命令仅告警不阻断,管理员可批量远程执行 | 默认阻断高危模式;显式确认或 RBAC |
| S-11 | P2 | [`web/app/commands.html`](web/app/commands.html) L95,128 | `session_id` 嵌入 `onclick='...'` 未 JS 转义 | `data-*` + 事件委托 |
| S-12 | P2 | [`server/api/agent.py`](server/api/agent.py) L48-66 | API Key 用 `==` 比较,非常量时间 | `secrets.compare_digest` |
| S-13 | **P0** | [`tests/load_test.py`](tests/load_test.py) L18、[`tests/quick_load.py`](tests/quick_load.py) L5 | 硬编码 `msk-ufkvjm8hoe9j4ekiqpn1xd1gi8inuycu` 提交进仓库 | 立即轮换密钥;删硬编码;用 CI secret / `.env.test` |
| S-14 | **P0** | [`web/agent/agent.py`](web/agent/agent.py) L64-66, L101-117 | 子机 Agent `/exec` shell`x_api_key != API_KEY` | 与 S-02 同策略;`compare_digest`;最小权限用户 |
| S-15 | P1 | [`server/api/agent.py`](server/api/agent.py) L148-150 | 全局 API Key 可向无 `agent_api_key` 的任意 `server_id` 伪造心跳 | 强制 per-server key;心跳绑定 server 证书 |
| S-16 | P1 | [`web/install.php`](web/install.php) | 遗留 PHP 5 步安装,可写 `.env`/建表,与 FastAPI install 双轨 | 删除或 nginx deny;仅保留 install.html |
| S-17 | P1 | [`server/main.py`](server/main.py) + FastAPI 默认 | `/docs` `/openapi.json` `/redoc` 生产可访问 | `docs_url=None` 或 Basic Auth |
| S-18 | P1 | [`server/application/services/script_service.py`](server/application/services/script_service.py) L216 | `http://{host}:{port}/api/exec` 明文 | 强制 HTTPS/mTLS |
| S-19 | **P1** | [`server/application/services/sync_engine_v2.py`](server/application/services/sync_engine_v2.py) L234-236 | `sysctl -w {key}={safe_value}` / `echo {key}={safe_value}`:仅 `shlex.quote(value)`,含 `'` 的 value 可逃逸 shell 引号执行任意命令 | `shlex.quote(f"{key}={value}")` 或 argv 列表不经 shell |
| S-20 | **P1** | [`server/application/services/sync_engine_v2.py`](server/application/services/sync_engine_v2.py) L281-284 | `cp {backup_file} ... && sysctl --system``backup_file` 来自 `ls` 输出未转义 | 白名单路径 + `shlex.quote(backup_file)` |
**SQL 注入**: ORM/`select()` 为主,[`search.py`](server/api/search.py) 对 LIKE 通配符有转义,**未发现典型拼接 SQL 注入**。
---
## 3. 代码规范违规
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| C-01 | P2 | 多处 `web/app/*.html` | 项目规则要求禁止未转义 `innerHTML`,但 [`servers.html`](web/app/servers.html)、[`scripts.html`](web/app/scripts.html) 等仍违规 | 抽公共 `renderTableRow()`CI 检查 `innerHTML``${` 组合 |
| C-02 | P2 | [`web/app/*.html`](web/app/) | 每页重复定义 `esc()`[`layout.js`](web/app/layout.js) 有 `_esc()` 未统一导出 | `api.js` 导出 `esc`/`escAttr`,页面禁止私有副本 |
| C-03 | P3 | [`server/api/install.py`](server/api/install.py) L283-284 | async 路由内同步 `subprocess.run` 阻塞事件循环 | `asyncio.to_thread` |
| C-04 | P3 | 前端 | 部分页面裸 `fetch`(如 [`install.html`](web/app/install.html))绕过 `apiFetch` | 安装向导可例外,但应集中错误处理 |
---
## 4. 安全风险(非直接漏洞)
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| R-01 | P1 | [`server/infrastructure/database/crypto.py`](server/infrastructure/database/crypto.py) | 遗留 AES-CBC + SHA256(API_KEY) 兼容 PHP | 迁移期后移除 CBC;统一 Fernet |
| R-02 | P2 | [`server/config.py`](server/config.py) L40 附近 | `DATABASE_URL` 默认 `root:password` | 安装模式强制无默认密码;启动校验 |
| R-03 | P2 | 15× [`web/app/*.html`](web/app/) | jsDelivr 无 SRI,供应链篡改可全站 RCE | 自托管 + `integrity` + `crossorigin` |
| R-04 | P2 | [`deploy/nginx_https.conf`](deploy/nginx_https.conf) | 有 HSTS,无 CSP / X-Content-Type-Options | 逐步启用 CSP`script-src` 限制 |
| R-05 | P2 | [`scripts/sync_mysql_mcp_env.py`](scripts/sync_mysql_mcp_env.py) | 将 DB 密码写入 `.env``MYSQL_PASSWORD`,扩大泄露面 | MCP 独立只读/开发用户 |
| R-06 | P2 | [`scripts/grant_nexus_local.sql`](scripts/grant_nexus_local.sql) | `GRANT ALL` + `'%'` 仅适合本地;误用于生产风险高 | 文档标注「仅本地」;生产用最小权限 SQL |
| R-07 | P3 | [`server/application/services/auth_service.py`](server/application/services/auth_service.py) L147-153 | 旧 refresh token 格式仍兼容,重用检测弱于新格式 | 强制迁移、废弃旧 token |
| R-08 | P3 | [`requirements.txt`](requirements.txt) | 无 CI `pip-audit`/OSV 扫描 | GitHub Action 定期依赖审计 |
---
## 5. 性能问题
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| P-01 | P1 | [`server/api/agent.py`](server/api/agent.py) L216-217 | 心跳直写 MySQL2000+ 服务器 × 60s | 见 B-04 |
| P-02 | P2 | [`server/api/servers.py`](server/api/servers.py) L58-63 | 列表页每服务器一次 `redis.hgetall`N+1 | Redis pipeline / `MGET` 批量 |
| P-03 | P2 | [`server/api/websocket.py`](server/api/websocket.py) L293-307 | 告警冷却 `_last_alert_time` 进程内 dict,多 worker 重复 Telegram | Redis SETNX + TTL 去重 |
| P-04 | P3 | [`server/api/install.py`](server/api/install.py) | 安装阶段同步 subprocess | 见 C-03 |
| P-05 | Info | [`server/infrastructure/ssh/asyncssh_pool.py`](server/infrastructure/ssh/asyncssh_pool.py) | 连接池设计合理;需压测 2000+ 并发 SSH 上限 | 配置 max pool + 监控指标 |
---
## 6. 可用性问题
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| U-01 | P2 | [`web/app/api.js`](web/app/api.js) L35-37 | 空 `catch` 吞掉刷新/加载错误 | 统一 `toast('error', ...)` 或重试提示 |
| U-02 | P2 | [`server/infrastructure/database/crypto.py`](server/infrastructure/database/crypto.py) | 密钥错误时表现为 SSH 连接失败而非配置错误 | 见 B-07 |
| U-03 | P3 | [`web/app/settings.html`](web/app/settings.html) | 部分配置加载失败静默 | 与 U-01 统一错误 UX |
| U-04 | Info | 移动端 | [`layout.js`](web/app/layout.js) 已有 overlay;大表横向滚动需各页验证 | 关键页 E2E 截图验收 |
---
## 7. 可扩展性问题
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| E-01 | P1 | 认证架构 | 无全局 JWT 中间件,每路由靠 `Depends(get_current_admin)`,新路由易漏 | `main.py` 默认拒绝 + 白名单(install/health/agent/auth/login |
| E-02 | P2 | Agent 协议 | 全局单 Key + shell exec,难以多租户/多环境隔离 | per-server API key + scoped capabilities |
| E-03 | P2 | 配置三写 | install → `.env` + `config.php` + MySQL `settings`,漂移风险 | 单一真相源 + 同步脚本(已有部分,需校验任务) |
| E-04 | P3 | 前端 | 15 页独立 Alpine 内联脚本,无组件复用 | 逐步抽 `components/` 或轻量模块(非必须大重构) |
---
## 8. 优化点(非错误)
| ID | 位置 | 建议 |
|----|------|------|
| O-01 | [`server/api/search.py`](server/api/search.py) | LIKE 转义已做,可补全文索引或 ES(2000+ 资产搜索) |
| O-02 | [`web/app/layout.js`](web/app/layout.js) | 全局搜索已统一 `_esc()`,推广为唯一转义入口 |
| O-03 | [`server/background/heartbeat_flush.py`](server/background/heartbeat_flush.py) | 与 B-04 对齐后,可加批量大小/退避指标 |
| O-04 | 测试 | [`tests/`](tests/) 覆盖有限,优先补 auth/webssh/agent 集成测试 |
| O-05 | 文档 | 已有 [`docs/project/mysql-mcp-setup.md`](docs/project/mysql-mcp-setup.md);安全项可增 ADRWebSSH JWT、Agent exec |
---
## 架构风险总览
```mermaid
flowchart TB
subgraph attack [高优先级攻击面]
XSS[XSS localStorage]
WebSSH[IDOR WebSSH]
AgentExec[Agent shell exec]
APIKey[Global API_KEY reveal]
end
XSS --> TokenTheft[窃取 JWT]
TokenTheft --> WebSSH
TokenTheft --> APIKey
APIKey --> AgentExec
```
---
## 修复优先级(建议执行顺序)
1. **立即(P0/P1 安全)**: S-01/B-01 WebSSH 绑定;S-04/S-05 XSSS-02 Agent exec 收敛;S-03 API Key reveal**S-19/S-20 sync_engine sysctl 注入**
2. **本周(一致性与性能)**: B-04/P-01 心跳写路径;B-03 在线统计;P-02 Redis pipeline
3. **迭代(纵深防御)**: S-06/S-07 会话与 WSR-03/R-04 CDN/CSPE-01 全局鉴权中间件
4. **文档/流程**: O-04 测试;R-08 依赖审计;每项修复写 [`docs/changelog/`](docs/changelog/)
---
## 与「WSL Node 20」计划的关系
| 计划项 | 状态 |
|--------|------|
| `wsl_install_node20.sh` 代理修复 | 已实现 |
| `MYSQL_READONLY=false` + `--writable` | 已实现 |
| `grant_nexus_local.sql` | 已实现 |
| MCP/Node 验证 | 已通过 |
**无需再跑该安装计划。** 若 MCP 仍红,仅需 Cursor Reload + 检查 MySQL 服务,而非重复改代码。
---
## 下一步(需你确认后执行)
若你确认本审计报告,可在 **Agent 模式** 按优先级分批修复(建议先做 P0/P1 安全项,每项单独 changelog)。你可指定:
- 只修安全(S-01 ~ S-12
- 或「P0/P1 全部」
- 或指定 ID 列表
我不会再自动重复 WSL Node20 实施循环。
---
## 10. 如何强制「逐行审查」(Phase 2 操作规程)
初版(Phase 1= **攻击面抽查 + 文件打勾**,不满足「逐行」承诺。要可复查、少漏报,必须单独开 **Phase 2**,并按下列规则验收。
### 10.1 你对助手说什么(复制即用)
切换到 **Agent 模式**(非 Plan),分批发:
```
执行审计计划 Phase 2:逐行复核。禁止攻击面抽查代替。
范围:server/**/*.py(本轮仅 server,完成后再 web)。
方法:
1. 用 rg 列出本目录每个文件的:命令拼接、subprocess、exec_ssh、WebSocket、SQL text(、硬编码密钥
2. 对每个命中行 Read 上下文 ±15 行,判定 safe / finding
3. 每个文件输出:
- [path] 行数 N | rg 命中 M 条 | 全部 closure 后才能标「逐行完成 ✓」
- findings 表:行号 | 严重度 | 描述
4. 禁止写「已审计」除非该文件 M=0 或每条命中都有 closure 记录
5. 每完成 5 个文件汇报一次,不要一次声称全库完成
从 server/application/services/sync_engine_v2.py 开始(已知漏报)。
```
修代码与审计**分两次会话**,避免混在一起。
### 10.2 「逐行」的可验收定义(不是读每一行散文)
| 必须做 | 说明 |
|--------|------|
| 全文 Read 或按 200 行分段 Read 至 EOF | 每个 `.py` 至少覆盖 100% 行号范围 |
| 全文件 rg 规则表(见 10.3) | 每个命中 = 一条待 closure 项 |
| 外部输入追调用链 | API → service → ssh/subprocess 到顶 |
| 产出 `docs/reports/audit-line-coverage-*.md` | 复查 AI 只认文件:行,不认「✓」 |
**不算逐行**:只 grep 几个关键词就标 ✓;只读 Top 15;子代理汇总不落行号。
### 10.3 每个 Python 文件必跑的 rg 规则(closure 前不得标完成)
```bash
# 在单个文件或目录执行,命中全部要 Read 上下文
rg -n "f[\"'].*\{.*\}.*(exec|run|shell|ssh|sysctl|curl|wget|sudo|bash|sh -c)" server/
rg -n "subprocess\.|create_subprocess_shell|os\.system|popen" server/
rg -n "exec_ssh_command|conn\.run\(|asyncssh" server/
rg -n "text\(|execute\(|raw\(" server/
rg -n "Depends\(get_current_admin\)|APIRouter|@router\.(get|post|put|delete|websocket)" server/api/
rg -n "HTTPException|401|403|API_KEY|compare_digest|secrets\." server/
```
前端另表:`innerHTML``localStorage``WebSocket.*token``eval(`
### 10.4 推荐拆分顺序(避免一次吹「全库完成」)
| 批次 | 路径 | 约行数 |
|------|------|--------|
| 2a | `server/api/*.py` | 高 |
| 2b | `server/application/services/*.py` | 高(含 sync_engine |
| 2c | `server/infrastructure/**/*.py` | 中 |
| 2d | `server/background/*.py` + `main.py` + `config.py` | 中 |
| 2e | `web/app/*.{html,js}` | 高 |
| 2f | `web/agent/*` + `scripts/*` + `tests/*` | 中 |
每批结束交付:`audit-phase-2{X}.md`(仅 findings + closure 表)。
### 10.5 你可加的 Cursor 规则(防止再抽查)
`.cursor/rules/audit-line-review.mdc` 中写:
- 安全审计任务:**禁止**在未列出 `文件:行号` 的情况下声称完成
- **禁止**使用「已审计 ✓」除非附带 rg 命中 closure 表
- 发现命令拼接必须引用 `shlex`/argv 或标为 finding
- 单次回复最多声称完成 **5 个文件** 的逐行审查
### 10.6 你怎么验收助手是否真的逐行
1. 抽查:打开 `sync_engine_v2.py`,看报告是否含 **234、236、284**S-19/S-20
2. 要求给 `wc -l` 与「本文件 Read 行范围 1-N」
3. 要求 `rg 命中数 = closure 数`,差 1 条即打回
4. 用另一只 AI 只喂 `audit-phase-*.md` + 源码,做交叉验证
### 10.7 Phase 2 todos(待你确认后执行)
| id | 内容 |
|----|------|
| phase2-api | `server/api/*.py` 全文 + rg closure |
| phase2-services | `server/application/services/*.py` 含 sync_engine 全行 |
| phase2-infra | `server/infrastructure/**` |
| phase2-web | `web/app` innerHTML + token |
| phase2-report | 合并为 `docs/reports/audit-line-coverage-2026-05-22.md` |
---
## 11. 逐行走读标准(SSOT — 唯一验收依据)
**独立文件(已落地)**
| 文件 | 用途 |
|------|------|
| [`docs/project/line-walk-audit-standard.md`](docs/project/line-walk-audit-standard.md) | 完整标准(推荐给人与复查 AI) |
| [`.cursor/rules/audit-line-review.mdc`](.cursor/rules/audit-line-review.mdc) | Cursor 编辑 `server/`/`web/` 时自动加载 |
本节是 Phase 2 的**正式标准**(与上表同步)。与 Phase 1(攻击面抽查)严格区分;未满足 **DoD** 的文件不得标记「逐行完成」。
### 11.1 术语
| 术语 | 定义 |
|------|------|
| **走读** | 对单文件按行号顺序阅读源码(可分段),并对规则命中点做上下文分析 |
| **触达** | 仅打开/ grep 过文件,未满足走读 DoD |
| **规则命中** | `rg` 规则表(11.4)在某行产生的匹配 |
| **Closure** | 对一条规则命中的判定记录:`SAFE``FINDING`(含行号、理由) |
| **DoD** | 单文件完成定义(11.6),全部满足才可标「逐行完成 ✓」 |
### 11.2 适用范围
| 包含 | 排除 |
|------|------|
| `server/**/*.py` | `docs/**` 仅做密钥扫描(11.8 |
| `web/app/*.{html,js}` | `.cursor/skills/**` |
| `web/agent/**` | 第三方 minified 库 |
| `scripts/**``deploy/**``tests/**` | 图片、字体、lock 文件 |
### 11.3 单文件走读流程(强制 8 步)
对清单中**每一个**源文件依次执行,不得跳步:
```
Step 1 登记:记录 path、语言、wc -l 行数 N
Step 2 全文覆盖:Read offset=1, limit=200 循环至 EOF(末段 offset 必须 ≥ N-5
Step 3 规则扫描:对该文件跑 11.4 中「本语言」全部规则,得到命中列表 H[]
Step 4 Closure:对 H[] 中每条命中 Read ±15 行,填写 11.5 表格一行
Step 5 入口梳理:若为 API/路由/WS,列出对外入口表(方法、路径、鉴权方式)
Step 6 数据流:标出「外部输入 → 危险 sink」(SQL/shell/文件/innerHTML/出站请求)
Step 7 八类核对:按 11.7 勾选本文件涉及的类别(无则勾 NONE)
Step 8 DoD 检查:满足 11.6 后写入报告,标记「逐行完成 ✓」
```
**单会话上限**:最多完成 **5 个文件** 的 Step 18,防止敷衍。
### 11.4 规则表(必须 100% Closure
#### Python`server/`、`scripts/`、`tests/`、`web/agent/*.py`
| 规则 ID | rg 模式(示意) | 审查要点 |
|---------|----------------|----------|
| PY-01 | `f["'].*\{.*\}.*` 且行内含 exec/run/shell/ssh/sysctl/curl/sudo/bash | 是否经 shell?变量是否 shlex.quote **整参数** |
| PY-02 | `subprocess\.|create_subprocess_shell|os\.system|popen` | shell=True?命令来源? |
| PY-03 | `exec_ssh_command|conn\.run\(` | asyncssh 是否默认 shell?拼接方式? |
| PY-04 | `text\(|execute\(|raw\(` | SQL 是否拼接用户输入? |
| PY-05 | `@router\.|APIRouter|websocket` | 是否有 `Depends(get_current_admin)` 或等效? |
| PY-06 | `API_KEY|SECRET|password|token` 字面量 | 是否硬编码密钥? |
| PY-07 | `pickle|yaml\.load\(|eval\(|exec\(` | 反序列化 / 动态执行 |
| PY-08 | `==.*[Kk]ey|!=.*[Kk]ey` | API Key 比较是否 timing-safe |
| PY-09 | `except.*:.*pass|except Exception` | 是否吞掉安全相关异常 |
| PY-10 | `open\(|Path\(.*write|unlink` | 路径遍历、任意写 |
#### 前端(`web/app/*.{html,js}`
| 规则 ID | rg 模式 | 审查要点 |
|---------|---------|----------|
| FE-01 | `innerHTML|outerHTML|insertAdjacentHTML` | 每个 `${}` 是否 esc / textContent |
| FE-02 | `localStorage|sessionStorage` | 是否存 JWT/密钥 |
| FE-03 | `WebSocket.*token|\.token=` | Token 是否在 URL |
| FE-04 | `fetch\(|apiFetch` | 是否绕过统一鉴权/错误处理 |
| FE-05 | `onclick=.*\$\{|javascript:` | 属性上下文注入 |
| FE-06 | `cdn\.|script src=` | SRI、供应链 |
#### Shell / SQL / Deploy
| 规则 ID | 范围 | 要点 |
|---------|------|------|
| SH-01 | `*.sh` | 未引用变量、curl 明文密钥 |
| DP-01 | `deploy/nginx*.conf` | CSP、TLS、限流 |
| SQL-01 | `*.sql` | GRANT 过大、'%' host |
**无命中**:在 Closure 表写 `H=0(规则表零命中)`,仍须完成 Step 2 全文 Read。
### 11.5 Closure 记录格式(每条命中一行)
```markdown
| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由(≤1句) |
|------|------|------|------|--------|--------------|
| sync_engine_v2.py | 236 | PY-01 | FINDING | P1 | sysctl -w 仅 quote value' 可断 shell |
| sync_engine_v2.py | 95 | PY-01 | SAFE | — | rsync 路径双端 shlex.quote |
```
- **SAFE** 必须写清依据(如「整段 argv」「Pydantic 校验」「仅常量」)
- **FINDING** 必须对应 11.7 八类之一,并给修复方向(≤1 句)
### 11.6 单文件 DoD(全部打勾才算完成)
- [ ] Step 2Read 覆盖行号 `1 .. N`(报告注明 N 与最后 Read 的 offset/limit
- [ ] Step 34`len(H) == Closure 行数`(含 SAFE + FINDING
- [ ] Step 5:有对外入口则必有「入口表」;纯 `__init__.py` 可写 N/A
- [ ] Step 6:已标外部输入与 sink,或写「无外部输入」
- [ ] Step 7:八类核对已勾
- [ ] 报告含「逐行完成 ✓」且**无** Phase 1 式笼统描述
### 11.7 问题八类与严重度(与 KPI 对齐)
| 类别 | 代号 | P0 示例 | P1 示例 | P2 示例 |
|------|------|---------|---------|---------|
| 功能缺陷 | BUG | — | 统计与列表数据源不一致 | 空 catch 吞错误 |
| 安全漏洞 | VULN | 未授权 RCE、IDOR 拿 Shell | XSS、密钥回读、命令注入 | CSRF 缺失 |
| 规范违规 | STYLE | — | 违反项目 esc 规则 | 重复 esc 实现 |
| 安全风险 | RISK | 仓库内生产 API Key | 明文 HTTP 调 Agent、弱 crypto | 无 SRI |
| 性能 | PERF | — | 心跳直写 MySQL、Redis N+1 | 阻塞 subprocess |
| 可用性 | UX | — | — | 加载失败无提示 |
| 可扩展性 | EXT | — | 无全局鉴权中间件 | 配置三写漂移 |
| 优化点 | OPT | — | — | 可合并重复逻辑 |
**KPI(复查用)**P0 漏报 -100 / P1 漏报 -50;每条 FINDING 必须 `文件:行号` 可定位。
### 11.8 批次交付物模板
每个批次一个文件:`docs/reports/audit-phase-2{X}-line-walk.md`
```markdown
# Phase 2{X} 逐行走读 — {目录}
## 文件清单
| 文件 | 行数 N | 命中 H | Closure | 状态 |
|------|--------|--------|---------|------|
## Findings(仅 FINDING
| ID | 文件:行 | 类 | 度 | 描述 | 修复建议 |
## Closure 全表(含 SAFE
11.5 格式)
## 入口表(api 批次必填)
| 方法 | 路径 | 鉴权 |
## 走读签字
- 范围:...
- 完成文件数 / 计划文件数
- 未含「攻击面 Top N」字样:是/否
```
### 11.9 禁止行为(违反即打回)
1. 未列 `文件:行号` 的 FINDING
2. `rg 命中数 ≠ Closure 数` 仍标完成
3. 用子代理「Top 15」代替 Closure 全表
4. 只写「已审计 ✓」无 Step 2 行号范围
5. 单回复声称完成整库 `server/` 或全部 `web/`
6. 审计与修代码同一任务混合(除非用户明确要求)
### 11.10 与 Phase 1 的关系
| Phase 1 | Phase 2 |
|---------|---------|
| 攻击面抽查、快速风险图 | 逐行走读、可复查 closure |
| 可发现「显眼」P0 | 可发现「单文件局部」拼接类 P1(如 sysctl) |
| 报告在计划 §0–§8 | 报告在 `docs/reports/audit-phase-2*.md` |
**原则**Phase 1 的 FINDING 在 Phase 2 须逐条 **VERIFY**(行号仍成立或降级/关闭);Phase 2 可新增 FINDING,不得删减已确认行号而不说明。
+44
View File
@@ -0,0 +1,44 @@
---
description: 减少盲目搜文件、控制 Read 次数与 token 消耗
alwaysApply: true
---
# Agent 探索约束(省 token
## 开始前
1. 读根目录 `AGENTS.md` 的「代码地图」与「问题 → 文件」表
2. 架构/API 全览只读 `docs/project/nexus-functional-development-guide.md`(或用户指定的 §),禁止为找结构全库 Grep
3. 用户已给出路径/文件名时,只读该路径,禁止扩大范围
## 搜索顺序
```
AGENTS.md / 功能指南 § → Grep(带 path)→ Read(≤3 个文件)→ 仍不够再扩一圈
```
- **禁止**无 `path` 的全仓库 `Grep` / `Glob`(除非用户明确要求全库)
- **Grep 优先于 Read**Glob 只用于定位文件名,不逐个 Read 全部匹配
- 同一任务 **Read 不超过 5 个文件**;找到答案后立即停止
- 不要重复 Read 已在本对话出现过的文件
## 分层定位(禁止跨层乱搜)
| 层 | 路径 |
|----|------|
| HTTP 路由 | `server/api/<module>.py` |
| 业务 | `server/application/services/` |
| 数据访问 | `server/infrastructure/` |
| 模型 | `server/domain/models/` |
| 前端页 | `frontend/src/pages/` |
| 前端路由 | `frontend/src/router/index.ts` |
| 前端 API 客户端 | `frontend/src/api/` |
真实前端源码在 `frontend/src/`,不是 `web/app/*.html`(那是构建产物)。
## 文档索引
- 功能 SSOT`docs/project/nexus-functional-development-guide.md`
- 文档导航:`docs/README.md`
- 本地路径/MCP`docs/project/linux-dev-paths.md`
- 运维(连机/exec):用户 Skill `Nexus平台` 或 `docs/project/nexus-1panel-operations-knowledge.md`
+66
View File
@@ -0,0 +1,66 @@
---
description: 安全/质量逐行走读审计 — Phase 2 closure 流程;禁止攻击面抽查冒充全量审计
globs:
- server/**
- web/app/**
- web/agent/**
- scripts/**
- deploy/**
- tests/**
---
# 逐行走读审计规则(Phase 2)
完整标准见:`standards/line-walk-audit-standard-v2.md`(权威)· Nexus SSOT`docs/project/line-walk-audit-standard.md` · 转接索引:`docs/project/standards-transfer-package.md`
## 何时启用
用户要求「全量审计」「逐行审查」「走读」「Phase 2」「安全审计」时,**必须**遵守本规则,不得用攻击面 Top N 或子代理汇总代替。
## 强制流程(每文件 8 步)
1. 登记 `path`、语言、行数 N`wc -l`
2. **全文 Read** 至 EOF(可每 200 行分段;末段须覆盖 N)
3. 对该文件跑 `docs/project/line-walk-audit-standard.md` §4 全部适用规则 → 命中列表 H
4. 对 H **每一条** Read ±15 行,填 ClosureSAFE / FINDING + 行号 + 理由)
5. API/WS 文件:写入口表(方法、路径、鉴权)
6. 标外部输入 → sinkSQL / shell / 文件 / innerHTML / 出站 HTTP
7. 八类归类:BUG / VULN / STYLE / RISK / PERF / UX / EXT / OPT
8. 满足 DoD 后标「逐行完成 ✓」
**单会话上限**:最多 **5 个文件** 完成 8 步。
## DoD(缺一不可)
- `len(H) == Closure 行数`H=0 须写明「规则零命中」)
- Read 覆盖 `1..N`(报告写明 N 与 Read 范围)
- 每条 FINDING 含 **`文件:行号`**
- 交付 `docs/reports/audit-phase-2*-line-walk.md`(含 Closure **全表**,含 SAFE
## 命令拼接(高频漏报)
- `f"{key}={shlex.quote(value)}"` **不等于**安全:须 `shlex.quote(f"{key}={value}")` 或 argv 不经 shell
- `ls`/`find` 输出拼进 `cp`/`rm` 须 **白名单 + shlex.quote**
- `exec_ssh_command` / `conn.run(command)` 默认经远程 shell
## 禁止
- 无行号的 FINDING
- 「已审计 ✓」无 Closure 表
- 单回复声称整库 `server/` 或全部 `web/` 完成
- 子代理只出 Top 15 不落行号
- 同一任务混审计与改代码(除非用户明确要求)
## 严重度(复查 KPI
- **P0**:未授权 RCE、IDOR Shell、仓库内生产密钥等
- **P1**:需认证后的命令注入、XSS、密钥回读等
- P0 漏报 -100 / P1 漏报 -50(用户复查约定)
## 报告路径
| 用途 | 路径 |
|------|------|
| 标准 SSOT | `docs/project/line-walk-audit-standard.md` |
| 批次产出 | `docs/reports/audit-phase-2{a-f}-line-walk.md` |
| Phase 1 参考 | `.cursor/plans/nexus_全面代码审计_a33e6fc2.plan.md` §0–§8 |
+38
View File
@@ -0,0 +1,38 @@
---
description: Nexus 前端规范 — Tailwind v4 + Alpine.jsapi.js JWT;禁止 XSS
globs: web/app/**
alwaysApply: false
---
# Nexus 前端 (Tailwind + Alpine.js)
## 页面结构
- 业务页先引入:`/app/api.js` → `/app/layout.js`(侧栏、搜索、移动端 overlay)
- 15 个 HTML 页面同级放在 `web/app/`,保持导航一致
## API 调用
- 一律 `apiFetch()` / `apiHeadersJSON()`,禁止手写裸 `fetch` 漏 Authorization
- Token 存 `localStorage``access_token`、`refresh_token`、`token_expires`
- 到期前 2 分钟自动 refresh401 重试一次后 `_logout()`
- 8 小时无活动会话超时(与后端 JWT `updated_at` 对齐)
## WebSocket
- 仪表盘 `/ws/alerts?token=...`JWT 查询参数,ADR-011
- 收到 `alert` / `recovery` 刷新 stats,勿阻塞 UI 线程
## 安全(XSS
- 禁止 `${userInput}` 直接插入 `innerHTML` 或 Alpine `x-html`
- 用户/服务器名等用 `textContent` 或 Alpine 文本绑定
## WebSSH
- `terminal.html`xterm.js + Koko 协议;resize 随窗口
- 关闭码 4003 = 凭据/授权失败
## 验收
改 UI 后浏览器实机验证:Console 无报错、Network 无 404/500、登录→核心流程可走通。
+20
View File
@@ -0,0 +1,20 @@
---
description: 远程 Linux 路径必须用 POSIX,禁止 os.path 拼接
globs: server/**/*.py
alwaysApply: false
---
# 远程路径 = POSIX only
## 规则
- **SSH / SFTP / rsync 目标路径**(含 `target_path`、`remote_path`、命令里的文件路径):
- 使用 `server.utils.posix_paths``normalize_remote_abs_path`、`remote_join`、`posix_join` 等)
- **禁止** `os.path.join` / `dirname` / `basename` / `normpath` 处理这类字符串
- **Nexus 本机真实 I/O**`open`、`os.walk`、`os.scandir`):可用 `os.path`,逻辑拼接仍优先 `posix_join`
- **仓库本地路径**`Path(__file__)`、MCP `DEPLOY_DIR`):可用 `os.path` / `pathlib`
## 参考
- 实现:`server/utils/posix_paths.py`
- 审计:`docs/audit/2026-06-01-posix-path-batch-review.md`
+46
View File
@@ -0,0 +1,46 @@
---
description: Nexus Python/FastAPI 后端规范 — 异步、Pydantic、Repository、UTC 时间
globs: server/**/*.py
alwaysApply: false
---
# Nexus Python 后端
## 依赖版本(勿随意降级)
FastAPI 0.115 · SQLAlchemy 2.0 async · aiomysql · Pydantic v2 · asyncssh 2.17 · redis 5.x · PyJWT 2.10
## API 路由
- 请求/响应模型定义在 `server/api/schemas.py`
- 业务 API 必须 `Depends(get_current_admin)`Agent 用 `X-API-Key`install 路由无 JWT
- 返回结构化错误,禁止裸 `except: pass`
## 数据库
- 模型:`server/domain/models/__init__.py`14 表)
- 访问:`*_repo.py`,分页在 Repository 层 `offset/limit/count`
- 时间戳:`datetime.now(timezone.utc)`,禁止 naive datetime
- 连接池:默认 pool=160、overflow=120(安装向导按 MySQL `max_connections` 计算)
## Redis
- 客户端:`server/infrastructure/redis/client.py`,启动时强校验(ADR-009
- 心跳 key`heartbeat:{server_id}` TTL 600;告警 key`alerts:{server_id}` TTL 3600
- 跨 worker 广播:Redis Pub/Sub `nexus:alerts`
## SSH
- 仅用 **asyncssh**(已删除 paramiko
- 连接池引用计数:`acquire` / `release`,空闲 5min 清理
- 远程命令参数必须 `shlex.quote()`
## 凭据加密
- Fernet 存储 `Server.password` / `ssh_key_private`
- API 响应:`password_set` 布尔,禁止返回明文密码
## 测试
- `pytest` + `tests/conftest.py`;改 API/Service 须跑相关测试
- 新 endpoint 至少覆盖:认证失败、校验失败、成功路径
+44
View File
@@ -0,0 +1,44 @@
---
description: Nexus 安全规范 — JWT/TOTP/API Key/Fernet/CORSMCP MySQL 只读
globs: server/api/**,server/application/**,server/infrastructure/database/**,web/app/**
alwaysApply: false
---
# Nexus 安全
## 认证矩阵
| 调用方 | 机制 |
|--------|------|
| 管理端 API | JWT Bearer + 可选 TOTP |
| Agent | `X-API-Key`(仅 per-server `agent_api_key`;经 `POST /api/servers/{id}/agent-key` 生成,BL-06 起无 global 回退) |
| WebSocket | JWT query param |
| 安装向导 | 无 JWT(仅安装模式可用) |
## 登录防暴破
- `auth_service.py`:失败计数 → 15min 锁定 → HTTP 429
- 刷新令牌重用检测:旧 refresh 作废、`token_version` 递增
## 密钥(禁改 / 禁提交)
- `.env` 永不入 git`SECRET_KEY` / `API_KEY` / `ENCRYPTION_KEY` / `DATABASE_URL` 不可从 settings 表覆盖
- UI 不展示可改的 `API_KEY`
## 运维信任模型
- 鉴权管理员即信任用户,不做 RBAC
- 必须防御:SQL/命令注入、XSS、凭据泄露
- Shell`shlex.quote()`DELETE/UPDATE 须有 WHEREMCP 同样)
## Cursor MySQL MCP
- 使用 `@yclenove/mysql-mcp-server`**默认 `MYSQL_READONLY=true`**
- `MYSQL_DATABASE_ALLOWLIST=nexus`(或实际库名)
- 禁止在 MCP `env` 或 git 中提交生产密码;连接信息仅写在本地 `.env`
- 查库优先 MCP `query` / `describe_table`,避免手写破坏性 SQL
## 审计
- 所有 CUD 写 `audit_logs`(含 `ip_address`
- WebSSH 会话与 `command_logs` 可追溯
+45
View File
@@ -0,0 +1,45 @@
---
alwaysApply: true
---
# Nexus 6.0 技术栈
## 架构分层(禁止跨层直连)
```
web/app/*.html → server/api/*.py → application/services → infrastructure/* → domain/models
```
- API 层:路由 + Pydantic 校验 + `Depends(get_current_admin)`,不写 SQL
- Service 层:业务编排,通过 Repository 访问数据
- InfrastructureDB/Redis/SSH/Telegram 实现细节
- DomainSQLAlchemy ORM 模型,无 FastAPI 依赖
## 启动模式
- 无 `.env` → **安装模式**:仅 `/api/install/` + `/app/install.html`
- 有 `.env` → 正常模式:`SECRET_KEY` / `API_KEY` / `ENCRYPTION_KEY` 缺一不可
## 配置三写(安装向导)
`web/data/config.php` + `.env``NEXUS_` 前缀)+ MySQL `settings` 表。
**不可从 DB 覆盖**`SECRET_KEY`、`API_KEY`、`ENCRYPTION_KEY`、`DATABASE_URL`。
## 数据流
Agent(60s) → Redis 实时 → 前端直读 → 10min → MySQL 历史。告警:WebSocket + Telegram。
## 后台任务(仅 primary worker
`heartbeat_flush`(10min) · `self_monitor`(30s) · `schedule_runner`(60s) · `retry_runner`(5min)
## HTTP / DB 会话
- 普通 API`DbSessionMiddleware` → `request.state.db`,禁止在 factory 里 `AsyncSessionLocal()` 后不 close
- WebSocket / `/api/install/`:不走 DB 中间件
## 相关规则文件
- `nexus-python-backend.mdc` — `server/**/*.py`
- `nexus-frontend.mdc` — `web/app/**`
- `nexus-security.mdc` — 安全与凭据
- `perfect-implementation.mdc` — 完美实现与文档纪律
+29
View File
@@ -0,0 +1,29 @@
---
description: 单 Agent 对话——禁止部门/Skill 分派,用户只与当前助手直接协作
alwaysApply: true
---
# 单 Agent 对话(强制)
Nexus 只保留**你 ↔ 当前 Cursor 助手**这一条协作链。已删除:`docs/skills/`、`docs/team/`、`.cursor/skills/`;本机 `~/.claude/skills/` 中凡带 `group: 管理组|工程部|测试部|产品部|设计部` 的目录也已移除。
## 禁止
- 扮演或「转交」管理组、工程部、测试部、产品部、设计部及项目经理/总监/`*-department` 等任何虚拟角色。
- 未经用户明确要求时,**不要** `Read` 或按 `available_skills` 去加载 Skill 文件(含全局 `~/.claude/skills/`、`~/.cursor/skills-cursor/`)。
- 用 Task/子代理模拟「开会、分派、部门审批」;子代理仅用于用户明确要求的并行搜代码/跑命令等**技术任务**,且不得冠部门名。
- 在回复里写「已转交 XX 部」「请以产品经理身份」等话术。
## 允许
- 直接:澄清需求 → 设计/技术文档(`docs/design/`)→ 改代码 → 跑 `tests/` / 门控 → changelog。
- 依据:`standards/`、`.cursor/rules/`(含 `perfect-implementation`、`nexus-*`、`audit-line-review`)。
## 测试(强制)
- **所有测试与「是否修好」由本 Agent 执行并留证据**(命令、生产 URL、浏览器、`docs/reports/*-verification.md`)。
- **不得**把未验证项推给用户代测;用户只做 Agent 交付后的**终验**(见 `docs/reports/2026-06-01-final-acceptance-checklist.md`)。
## 默认协作
用户说什么就做什么;需要方案时在同一对话里说明取舍,不引入额外「智能体」层。
+62
View File
@@ -0,0 +1,62 @@
---
description: 完美实现原则 — 禁止降级/妥协/技术债;设计文档与技术文档先行;每次修改写 changelog
alwaysApply: true
---
# 完美实现与文档纪律
## 实现标准(强制)
**所有设计和实现都必须是完美实现。**
- ❌ **不允许为了实现而实现** — 每一行代码、每一个接口必须有明确目的,达到项目既定的安全、架构与质量标准
- ❌ **不允许降级实现** — 不得以「先用着」「以后再改」为由降低安全、性能、可维护性或用户体验
- ❌ **不允许妥协实现** — 发现问题必须根治,不得用绕过、静默吞错、硬编码掩盖代替修复
- ❌ **不允许留技术债** — 不得留下 TODO/FIXME/「临时方案」延后处理;当场能修必须当场修
- ✅ **无法完美实现时必须停下** — 与用户沟通并确认替代方案后,才能换方案;不得擅自从权或自行降级
## 文档纪律(强制)
### 设计文档
- 所有**新功能、架构变更、非平凡重构**必须先有设计文档,再写代码
- 设计文档路径:`docs/design/specs/YYYY-MM-DD-<topic>-design.md`
- 设计文档至少包含:背景与目标、方案对比、选定方案及理由、接口/数据模型、安全与性能约束、验收标准
### 技术文档(开发前)
- 开发前输出技术文档(实施说明),路径:`docs/design/plans/YYYY-MM-DD-<topic>.md` 或 `docs/reports/<step>/engineering-report.md`
- 技术文档至少包含:涉及文件清单、实现步骤、依赖与配置变更、测试要点、回滚方式
- 项目里**已有模块但缺文档的,在改动时一并补全**,不得只改代码不留档
### Changelog(每次修改)
- **每一次**有意义的代码或配置修改,必须在 `docs/changelog/` 新增或更新记录
- 命名:`YYYY-MM-DD-<简短主题>.md`
- 每条记录至少包含:日期、变更摘要、动机、涉及文件、是否需迁移/重启、验证方式
- 禁止把 changelog 写在 commit message 里代替项目文档
## 执行顺序
```
需求确认 → 设计文档 → 技术文档 → 实现 → 测试验证 → changelog →(必要时)更新 CLAUDE.md/AGENTS.md
```
## 禁止示例
```python
# ❌ 技术债
# TODO: fix later
pass # workaround
# ❌ 降级
except Exception:
return None # 静默失败
# ❌ 无文档直接大改
# (新增整模块却无任何 docs/design 或 docs/changelog 记录)
```
## 允许的唯一例外
经用户**明确书面确认**后,方可采用非完美方案;须在 changelog 中记录例外原因、影响范围与后续补齐计划。
+7
View File
@@ -0,0 +1,7 @@
{
"plugins": {
"compound-engineering": {
"enabled": true
}
}
}
+56
View File
@@ -0,0 +1,56 @@
# Nexus 6.0 — Cursor 速查
> SSOT: [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md)
> Handoff: [docs/project/AI-HANDOFF-2026-06-03.md](docs/project/AI-HANDOFF-2026-06-03.md)
## 栈
FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA15 页)· Vite → `web/app/`
## 环境(勿在仓库写密码)
- 生产: `https://api.synaglobal.vip` · 端口 8600 · `ssh nexus`
- 本地: `~/Nexus` 或 `$NEXUS_ROOT` · 见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)
- 凭据: 仅 `.env`(不入 git
- **Gitea**clone/pull 可匿名;**push** 用 `bash scripts/git-push.sh`(凭据在 `deploy/nexus-1panel.secrets.sh`,已 gitignore,勿提交)
## 关键路径
| 用途 | 路径 |
|------|------|
| 后端入口 | `server/main.py` |
| 前端路由 | `frontend/src/router/index.ts` |
| API 客户端 | `frontend/src/api/index.ts` |
| Sync | `server/api/sync_v2.py` |
| 门控 | `deploy/pre_deploy_check.sh` |
| 本地验证 | `bash scripts/local_verify.sh` |
## 铁律
见 `.cursor/rules/perfect-implementation.mdc` · `nexus-security.mdc` — 完美实现、无静默吞错、无明文密钥、CUD 审计。
## Agent Git / diff(省 token、防卡顿)
- **禁止**无路径过滤的全仓 `git diff` / `git diff HEAD``web/app/assets` 构建产物可导致数十万行输出)。
- **优先**`git diff --stat` 或 `git diff -- frontend/src server/api tests docs`。
- **提交**:只 `git add` 源码与文档;`web/app/assets/*` 由 Docker 内 `vite build` 生成,勿为 diff 把海量 hash 文件纳入上下文。
- **部署/门控**:定向 pytest(如 `tests/integration/test_*.py`),避免并行全仓 diff + 本地 build + 远程镜像重建。
## 部署
默认 **本机 rsync 直传**(不 push Gitea):
```bash
bash deploy/pre_deploy_check.sh && bash deploy/deploy-production.sh
```
仅同步源码(不重建镜像):`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/`
## 进度条(改代码时输出)
```
□实现 □本地验证 □审计8步 □部署 □健康检查 □浏览器验证 □changelog
```
+42
View File
@@ -0,0 +1,42 @@
.git
.gitignore
.claude
.cursor
**/__pycache__
**/*.pyc
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
.coverage
htmlcov/
frontend/node_modules
frontend/dist
node_modules
.env
docker/.env
docker/runtime
tests/
docs/
standards/
*.md
!docker/README.md
web/uploads
backups/
deploy/gate_log.jsonl
# Release/build local artifacts
frontend-v2/node_modules
.venv
.venv-*
venv
.pytest_cache
.ruff_cache
.playwright-mcp
2025.2
=2025.2
*.bak-
tmp/
+16
View File
@@ -0,0 +1,16 @@
# EditorConfig — keep LF for code deployed to Ubuntu
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{bat,cmd}]
end_of_line = crlf
[*.md]
trim_trailing_whitespace = false
+11
View File
@@ -50,3 +50,14 @@ NEXUS_HEALTH_CHECK_INTERVAL=60
# ── Telegram Alerts (optional — configure in Settings UI) ──
NEXUS_TELEGRAM_BOT_TOKEN=
NEXUS_TELEGRAM_CHAT_ID=
# ── MySQL MCP (@yclenove/mysql-mcp-server — run: python scripts/sync_mysql_mcp_env.py) ──
# Cursor 从项目根 .env 读取;与 NEXUS_DATABASE_URL 保持一致;默认只读
# MYSQL_HOST=127.0.0.1
# MYSQL_PORT=3306
# MYSQL_USER=root
# MYSQL_PASSWORD=password
# MYSQL_DATABASE=nexus
# MYSQL_READONLY=true # production / MCP read-only
# MYSQL_READONLY=false # local dev: full DML/DDL via MCP
# MYSQL_DATABASE_ALLOWLIST=nexus
+28
View File
@@ -0,0 +1,28 @@
# Line endings: Ubuntu production + bash scripts require LF in git.
# Windows dev: set `git config core.autocrlf false` and rely on this file.
*.sh text eol=lf
*.py text eol=lf
*.vue text eol=lf
*.ts text eol=lf
*.mts text eol=lf
*.mjs text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.html text eol=lf
*.js text eol=lf
*.json text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.toml text eol=lf
*.ini text eol=lf
*.sql text eol=lf
*.md text eol=lf
*.txt text eol=lf
.env.example text eol=lf
.gitattributes text eol=lf
.editorconfig text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
+42 -2
View File
@@ -35,18 +35,58 @@ jobs:
run: mypy server/ --ignore-missing-imports
continue-on-error: true # Type errors don't block deployment yet
- name: Run tests with coverage
- name: Run fast tests with coverage
run: |
coverage run -m pytest tests/ -v --tb=short
coverage run -m pytest tests/ -m "not chain and not slow" -q --tb=short
coverage report --fail-under=50
coverage xml
- name: Run chain tests
run: pytest tests/chain -m chain -q --tb=short
continue-on-error: true
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
# ── CI: E2E (main only, optional secrets) ──
e2e:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Install Playwright Chromium
working-directory: frontend
run: npx playwright install chromium
- name: Run Playwright E2E
working-directory: frontend
env:
NEXUS_E2E_BASE: ${{ secrets.NEXUS_E2E_BASE || 'http://127.0.0.1:8600' }}
NEXUS_E2E_USER: ${{ secrets.NEXUS_E2E_USER || 'admin' }}
NEXUS_E2E_PASSWORD: ${{ secrets.NEXUS_E2E_PASSWORD }}
run: |
if [ -z "$NEXUS_E2E_PASSWORD" ]; then
echo "NEXUS_E2E_PASSWORD not set — skip E2E"
exit 0
fi
npm run test:e2e
continue-on-error: true
# ── CD: Deploy ──
deploy:
needs: test # Only deploy if tests pass
+90 -52
View File
@@ -1,52 +1,90 @@
# Nexus — Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
.eggs/
# Environment (NEVER commit — contains production credentials)
.env
.install_locked
.install_state.json
SECRETS.md
.venv/
venv/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
nexus.log
# Test
.pytest_cache/
htmlcov/
.coverage
coverage.xml
# Node (for Tailwind/Alpine.js build)
node_modules/
web/css/tailwind-output.css
# Uploads
web/uploads/
# Data (sensitive)
web/data/config.php
web/data/*.db
# Deploy
deploy/*.key
# Nexus — Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
build/
.eggs/
# Environment (NEVER commit — contains production credentials)
.env
.install_locked
.install_state.json
SECRETS.md
.git-askpass-nexus.sh
deploy/nexus-1panel.secrets.sh
*.pem
.env.*
.venv/
.venv-*/
venv/
ENV/
# Agent memory (local MCP persistence, do not commit)
.claude/mcp-memory.jsonl
# MCP tool data (local development artifacts)
.megamemory/
.playwright-mcp/
frontend/test-results/
frontend/e2e/.auth/
tmp/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
nexus.log
# Node offline cache for WSL install (large tarball)
.cache/
# Test
.pytest_cache/
htmlcov/
.coverage
coverage.xml
# Node (for Tailwind/Alpine.js build)
node_modules/
web/css/tailwind-output.css
# Build output (Vite/Vuetify frontend)
# SPA 构建产物纳入 Git,便于服务器 git pull 部署(见 deploy/deploy-frontend.sh
# 仍忽略本地临时文件
web/app/assets/.tmp/
# Legacy dist dir (no longer used)
**/dist/
# Uploads
web/uploads/
# Data (sensitive)
web/data/config.php
web/data/*.db
web/data/1panel-hosts.json
# Backups
backups/
# Docker local secrets
docker/.env
docker/.env.prod
!docker/.env.example
!docker/.env.prod.example
scripts/npm-proxy.env
# Deploy
deploy/*.key
# Backups
backups/
+96
View File
@@ -0,0 +1,96 @@
# Nexus 6.0 — Agent 入口(薄 stub
> **长文 SSOT**: [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md) + [附录](docs/project/nexus-functional-development-guide-appendix.md)
> **会话接续**: [docs/project/AI-HANDOFF-2026-06-03.md](docs/project/AI-HANDOFF-2026-06-03.md)
> **文档总索引**: [docs/README.md](docs/README.md)
## 项目概要
Nexus2000+ 子机运维平台。后端 FastAPI + MySQL + Redis;前端 **Vue 3 + Vuetify 4 + TypeScript SPA**15 页,`frontend/src/pages/`);构建输出 `web/app/`;安装向导 `web/app/install.html`
## 本地开发
```bash
docker compose up -d mysql redis
bash scripts/start-dev.sh # API :8600
bash scripts/local_verify.sh # L2b
cd frontend && npm run dev # :3000/app/
```
路径见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)。
## Git(本机 + Gitea
- 改完代码:**`git commit``bash scripts/git-push.sh` 推到 Gitea**(凭据 `deploy/nexus-1panel.secrets.sh`);大仓库可用 `scripts/git-push-axs-full.sh`
- 本地 commit 仍用于回滚;**不要漏推 Gitea**(用户约定 2026-07-09)。
- `.env``SECRETS.md``*.pem``deploy/nexus-1panel.secrets.sh` 已在 `.gitignore`**勿提交**。
- 改代码仍写 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)。
## 强制约束
- 完美实现:`.cursor/rules/perfect-implementation.mdc`
- 安全 / 后端 / 前端:`.cursor/rules/nexus-*.mdc`
- 改代码必 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)
- 测试由 Agent 完成;用户只做终验
## 不可改
`API_KEY` · `SECRET_KEY` · `ENCRYPTION_KEY` · `DATABASE_URL`
## 部署
默认 **本机 rsync → SSH 生产机****代码先 push Gitea 再部署**(见上节 Git):
```bash
bash deploy/pre_deploy_check.sh
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。
## 归档文档
历史审计合并卷:`docs/archive/README.md`
## 代码地图(Agent 先读这里,勿全库搜)
```
server/main.py # 路由挂载入口
server/config.py # 配置(.env + settings 表)
server/api/*.py # HTTP/WebSocket 路由(按模块拆分)
server/application/ # 业务 services
server/infrastructure/ # DB / Redis / SSH / Telegram
server/domain/models/ # SQLAlchemy ORM
frontend/src/pages/ # Vue 页面(15 页 SPA
frontend/src/router/ # Hash 路由 #/
frontend/src/api/ # 前端 API 客户端
frontend/e2e/ # Playwright 验收
tests/test_*.py # pytest
docs/project/ # 功能/部署 SSOT
deploy/ # 生产部署脚本
scripts/ # 本地验证、MCP、门控
.cursor/mcp.json # mysql-mcp
```
## 问题 → 文件(常见锚点)
| 问什么 | 先看 |
|--------|------|
| 某 API 怎么实现 | `server/main.py` 找 router → `server/api/<模块>.py` |
| 服务器列表/搜索/exec | `server/api/servers.py`, `server/api/scripts.py` |
| 认证/JWT | `server/api/auth.py`, `server/api/dependencies.py` |
| Agent 心跳/安装 | `server/api/agent.py` |
| WebSSH/终端 | `server/api/webssh.py`, `server/api/terminal.py` |
| 同步/文件/搜索 | `server/api/sync_v2.py`, `files.py`, `search.py` |
| 设置/审计/告警 | `server/api/settings.py` |
| 前端某页 UI | `frontend/src/router/index.ts``frontend/src/pages/` |
| MySQL MCP | `.cursor/mcp.json`, `scripts/linux_mcp_mysql.sh` |
| 本地开发路径 | `docs/project/linux-dev-paths.md` |
| 14 页/API 全览 | `docs/project/nexus-functional-development-guide.md` §910 |
## 探索规则(省 token
详见 `.cursor/rules/agent-exploration.mdc`。要点:**先地图再 Grep(带 path)→ Read ≤5 个文件 → 停止**。
+22 -126
View File
@@ -1,132 +1,28 @@
# Nexus 6.0 — Project Memory
# Nexus 6.0 — Agent 入口(薄 stub
## 项目概述
Nexus 是一个 2000+ 服务器运维管理平台,从旧的 MultiSync (PHP + AdminLTE + pymysql) 完全重构为 Clean Architecture (FastAPI + Async SQLAlchemy + Redis + WebSocket + Telegram)。前端已全面迁移到 Tailwind CSS v4 + Alpine.jsPHP 依赖已移除。
> 与 [AGENTS.md](AGENTS.md) 相同入口;长文见 [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md)。
## 仓库结构
- **唯一仓库**: Nexus (Gitea, 仓库地址在 .env 中配置)
- **不再使用** multi-server-sync 仓库(旧项目,仅保留历史)
- **部署路径**: 由 `NEXUS_DEPLOY_PATH` 环境变量决定(安装向导自动写入 .env)
- **域名/CORS**: 由 `NEXUS_CORS_ORIGINS` 环境变量决定(安装向导自动写入)
## 快速链接
| 用途 | 路径 |
|------|------|
| 功能 / API / 14 页 | `docs/project/nexus-functional-development-guide.md` |
| 附录(sync 端点、调用链) | `docs/project/nexus-functional-development-guide-appendix.md` |
| 当前 handoff | `docs/project/AI-HANDOFF-2026-06-03.md` |
| 本地验证 | `bash scripts/local_verify.sh` |
| 验收 L0L5 | `docs/project/development-acceptance-standard.md` |
| 风险接受(单人运维) | `docs/project/risk-acceptance-single-operator.md` |
| 文档索引 | `docs/README.md` |
| 历史归档 | `docs/archive/README.md` |
## 栈
FastAPI · Vue 3 SPA · Hash 路由 `#/` · 门控 `deploy/pre_deploy_check.sh`7 道)
## 流程
## 目录结构 (仓库根目录)
```
Nexus/
├── server/ ← Python FastAPI后端 (Clean Architecture 4层)
│ ├── api/ ← API路由 (含 install.py 安装向导API)
│ ├── domain/ ← SQLAlchemy模型 (14张表)
│ ├── infrastructure/← SSH连接池 + Redis + Telegram
│ ├── background/ ← 后台任务 (心跳刷新/自检/调度/重试)
│ └── config.py ← Pydantic Settings + MySQL覆盖
├── web/app/ ← Tailwind+Alpine.js 纯静态前端 (12个HTML页面)
│ ├── install.html ← 安装向导 (5步)
│ ├── login.html ← JWT登录+TOTP双因素
│ └── ... ← index/servers/files/push/scripts等
├── deploy/ ← Supervisor配置 + Shell健康检查脚本
├── docs/ ← 项目文档
├── tests/ ← 测试
├── .env ← NOT IN GIT (安装向导生成)
└── requirements.txt ← Python依赖
实现 → local_verify → 审计(按 diff 量级)→ changelog →(批准)部署
```
## 启动模式
- **安装模式** (无 .env): 仅 `/api/install/` + 静态文件可用,其他路由返回503
- **正常模式** (有 .env): 完整初始化 DB/Redis/后台任务
## 已确认的关键决策
| 决策 | 选择 | 原因 |
|------|------|------|
| API_KEY | ❌ 禁改,UI不显示 | 加密一致性 + 子服务器认证 |
| SECRET_KEY | ❌ 禁改 | 加密回退密钥 |
| DATABASE_URL | ❌ 禁改 | 启动必需 |
| system_name/title | ✅ 可改 | 前端显示 |
| pool_size/overflow | ✅ 可改 | 安装向导自动推荐 |
| redis_url | ✅ 可改 | Redis连接 |
| 告警阈值 (CPU/mem/disk) | ✅ 可改 | 监控参数 (默认80%) |
| Telegram配置 | ✅ 可改 | 告警推送 |
| Agent心跳间隔 | 60s | 不太频繁 |
| Redis→MySQL刷新 | 10min | 降低MySQL负载 |
| 心跳数据流 | Agent→Redis→前端直读→10min→MySQL | 前端实时, MySQL只存历史 |
| 告警推送 | WebSocket→前端 + Telegram | 即时感知 |
| Python守护 | 3层: Supervisor + Python自检(30s) + Shell(cron 1min) | 全覆盖 |
| 安装向导 | install.html + FastAPI API | D2: 替代install.php |
| 配置源 | 安装向导→config.php(PHP兼容)+settings表(MySQL)+.env(Python) | 三写一致 |
| Agent告警心跳 | CPU/内存>80% 或变化>10% 时主动上报 | 智能监控 |
| 仓库统一 | 所有代码统一在Nexus仓库 | 不再用两个仓库 |
## 实现状态 (Phase A+B+C+D+E) — ~99% 完成
### ✅ 已完成 (全部6步 + 技术债务 + UX增强)
| 模块 | 文件 | 说明 |
|------|------|------|
| **Step 0: 基础设施** | | Redis + WebSocket |
| E1 WebSocket推送 | server/api/websocket.py | alert/recovery/system broadcast |
| E2 心跳→Redis+告警 | server/api/agent.py | Redis write + threshold detection |
| E3 Redis→MySQL刷新 | server/background/heartbeat_flush.py | 10min batch |
| **Step 1: 数据层** | | 14张表 |
| E4 load_settings_from_db | server/config.py | MySQL overrides mutable settings |
| **Step 2: 认证层** | | JWT + TOTP |
| **Step 3: Web SSH** | | asyncssh + xterm.js |
| E5 asyncssh连接池 | server/infrastructure/ssh/asyncssh_pool.py | 引用计数模式 |
| E6 WebSSH前端 | web/app/terminal.html | xterm.js + Koko协议 + 自动resize |
| **Step 4: Sync引擎** | | 4种同步模式 |
| **Step 5: 前端迁移** | | Tailwind+Alpine.js |
| E7 13个前端页面 | web/app/*.html | 含install.html安装向导 |
| E8 安装向导API | server/api/install.py | 6端点, 无JWT, 临时引擎 |
| E9 条件启动模式 | server/main.py | 无.env=安装模式 |
| **后台任务** | | |
| E10 Python自检 | server/background/self_monitor.py | 30s loop |
| E11 Telegram推送 | server/infrastructure/telegram/__init__.py | alert/recovery/system |
| E12 /health端点 | server/api/health.py | Shell health_monitor.sh checks this |
| **技术债务** | | |
| D1 ✅ paramiko删除 | — | pool.py已删除, 全部迁移asyncssh |
| D2 ✅ install迁移 | server/api/install.py | install.php→install.html+API |
| D3 ✅ WebSSH前端 | web/app/terminal.html | xterm.js+Koko协议+全屏 |
| **安全增强** | | |
| S1 JWT全局保护 | server/api/*.py | 所有业务API都有JWT验证 |
| S2 审计日志 | server/api/*.py | 所有CUD操作记录审计日志 |
| S3 登录防暴破 | server/application/services/auth_service.py | 失败计数+15min锁定+429状态码 |
| **前端UX增强** | | |
| UX1 共享布局 | web/app/layout.js | 11页面统一侧边栏+全局搜索+用户信息 |
| UX2 Toast通知 | web/app/api.js | 全局toast()函数,4种类型 |
| UX3 全局搜索 | server/api/search.py | 跨服务器/脚本/凭据/调度搜索 |
| UX4 服务器详情 | web/app/servers.html | 点击展开详情面板(系统信息/同步日志/SSH会话) |
| UX5 脚本编辑 | web/app/scripts.html | 点击脚本名编辑+执行结果格式化 |
| UX6 推送增强 | web/app/push.html | 逐服务器状态+进度条+推送历史 |
| UX7 审计分页 | web/app/audit.html | 分页控件+操作过滤+每页50条 |
| UX8 重试操作 | web/app/retries.html | 手动重试+删除按钮+状态过滤 |
| UX9 文件浏览 | web/app/files.html | 面包屑导航+目录排序+父目录链接 |
| UX10 登录安全 | web/app/login.html | 密码可见切换+失败计数+429锁定+shake动画 |
### 🔄 待测试
- T1: 心跳Redis写入验证
- T2: WebSocket连接测试
- T3: 3层守护测试 (kill Python→Supervisor重启)
- T4: Telegram推送测试
- T5: install.html 5步流程测试
### 测试流程 (用户确认)
1. WSL本地基础测试 → 2. 推到仓库 → 3. 外网服务器正式测试
## 配置三写机制
安装向导 Step 3 POST处理同时写入:
1. `web/data/config.php` — PHP兼容配置 (API_BASE_URL, DB_*, API_KEY)
2. `.env` — Python后端启动必需 (NEXUS_前缀)
3. `settings` MySQL表 — 共享动态配置 (api_key, thresholds, Telegram等)
不可改项 (SECRET_KEY/API_KEY/ENCRYPTION_KEY/DATABASE_URL) 不在MySQL覆盖映射中。
## 3层守护机制
- **Layer 1**: Supervisor — Python崩溃自动重启
- **Layer 2**: Python self_monitor — 每30s检查Redis/MySQL/WebSocket
- **Layer 3**: Shell health_monitor.sh — 每分钟HTTP GET /health, 连续3次失败→supervisorctl restart + Telegram
## 数据流
```
Agent心跳(60s) → Redis(实时) → 前端直读 → 10min批量 → MySQL(历史)
告警: CPU/mem/disk > threshold → WebSocket推送浏览器 + Telegram推送手机
恢复: 之前告警的指标恢复正常 → 自动推送恢复通知
Redis心跳key: heartbeat:{server_id} (HSET, TTL=600s)
Redis告警key: alerts:{server_id} (SET, TTL=3600s)
```
强制规则:`.cursor/rules/` · `standards/`
+33
View File
@@ -0,0 +1,33 @@
# Nexus 6.0 — API + 静态 SPA (web/app)
FROM python:3.12-slim-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
openssh-client \
rsync \
sshpass \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server/ ./server/
COPY web/app/ ./web/app/
COPY docker/entrypoint.sh ./docker/entrypoint.sh
RUN chmod +x ./docker/entrypoint.sh
ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
NEXUS_HOST=0.0.0.0 \
NEXUS_PORT=8600
EXPOSE 8600
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
+76
View File
@@ -0,0 +1,76 @@
# Nexus 6.0 — production image (frontend build + API)
# Build: docker compose -f docker/docker-compose.prod.yml build
# See: docs/design/plans/2026-06-04-1panel-docker-production.md
# ── Stage 1: Vue/Vite SPA ──
FROM node:20-bookworm-slim AS frontend
WORKDIR /build/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci --ignore-scripts
COPY frontend/ ./
RUN npx vite build
# ── Stage 1b: React/Vite App V2 ──
FROM node:20-bookworm-slim AS frontend_v2
WORKDIR /build/frontend-v2
COPY frontend-v2/package.json frontend-v2/package-lock.json ./
RUN npm ci --ignore-scripts
COPY frontend-v2/ ./
RUN npm run build:safe
# ── Stage 2: Python API + built static assets ──
FROM python:3.12-slim-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
openssh-client \
rsync \
sshpass \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server/ ./server/
COPY web/agent/ ./web/agent/
COPY --from=frontend_v2 /build/web/app-v2 ./web/app-v2/
COPY --from=frontend /build/web/app ./web/app/
# Login wallpaper cache (Vite does not emit this dir; sync also writes here at runtime)
COPY web/app/wallpapers/ ./web/app/wallpapers/
# Install wizard + static SPA assets not emitted by Vite
COPY web/app/install.html ./web/app/install.html
COPY web/app/servers_import_template.csv ./web/app/servers_import_template.csv
RUN mkdir -p ./web/app/vendor
COPY web/app/vendor/tailwindcss-browser.js \
web/app/vendor/theme-init.js \
web/app/vendor/theme.css \
web/app/vendor/alpinejs.min.js \
./web/app/vendor/
COPY docker/entrypoint.sh ./docker/entrypoint.sh
RUN chmod +x ./docker/entrypoint.sh \
&& test -f /app/web/app/install.html \
&& test -f /app/web/app-v2/index.html \
&& test -f /app/web/app/vendor/alpinejs.min.js \
&& test -f /app/web/app/vendor/tailwindcss-browser.js \
&& test -f /app/web/app/vendor/theme.css \
&& test -f /app/web/app/vendor/theme-init.js
ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
NEXUS_HOST=0.0.0.0 \
NEXUS_PORT=8600
EXPOSE 8600
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
+11
View File
@@ -0,0 +1,11 @@
# Nexus 6.0 项目交接(已 supersede
请以以下文档为准:
| 文档 | 路径 |
|------|------|
| 功能开发 SSOT | [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md) |
| 会话接续 | [docs/project/AI-HANDOFF-2026-06-03.md](docs/project/AI-HANDOFF-2026-06-03.md) |
| 文档总索引 | [docs/README.md](docs/README.md) |
历史全文:[docs/archive/project/PROJECT_HANDOVER.md](docs/archive/project/PROJECT_HANDOVER.md)
+189
View File
@@ -0,0 +1,189 @@
# Nexus 6.0
面向 **2000+ 服务器** 的运维管理平台:FastAPI + Async SQLAlchemy + Redis + WebSocket + Telegram(后端),Vue 3 + Vuetify 4 + TypeScript SPA(前端 14 页)。
| 项 | 说明 |
|----|------|
| 生产域名 | https://api.synaglobal.vip |
| 默认安装目录 | `/opt/nexus` |
| 后端端口 | `8600`1Panel 反代到 `http://127.0.0.1:8600` |
| 安装向导 | `https://你的域名/app/install.html` |
| 文档索引 | [docs/README.md](docs/README.md) |
生产 Docker 栈 **仅含 Nexus 容器**MySQL / Redis 在宿主机或 1Panel 自行安装。
---
## 一键命令速查(root · 公共仓库 · 无需令牌)
| 场景 | 一条命令 |
|------|----------|
| **仅装 Nexus**1Panel 已就绪) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" \| bash` |
| **重装服务器**1Panel + Docker + Nexus | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" \| bash` |
| **一键更新**pull + 重建镜像) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/update.sh" \| bash` |
| **强制无缓存重建**entrypoint 变更后) | `curl -fsSL ".../deploy/update.sh" \| bash -s -- --no-cache` |
| **已 clone** | `cd /opt/nexus && bash deploy/update.sh` |
指定资源档位(`1c4g` / `2c8g` 默认 / `4c16g`):
```bash
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash -s -- --profile 4c16g
```
**raw 地址**`http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/<脚本名>`
安装完成后浏览器打开安装向导,完成 MySQL(自建)/ Redis(自建)/ 管理员初始化。
---
## NX 统一运维菜单
装好后在服务器任意目录输入 **`nx`**`install` / `update` / 首次运行 `nx` 会自动注册 `/usr/local/bin/nx`):
```bash
nx # 统一菜单:安装 + 一键更新 + 重启 + 日志 + 备份
nx update # 非交互一键更新
nx update --no-cache
nx install-fresh # 全新安装
nx health # 健康检查
```
| 菜单项 | 作用 |
|--------|------|
| `[4] 一键更新` | pull + 重建 Docker 镜像(等同 `deploy/update.sh` |
| `[5] 重启 Nexus` | 重启 Python 应用 |
| `[6] 日志` | 跟踪容器日志 |
| `[1][2]` | 安装 / 全新安装向导 |
全局命令(符号链接):
| 命令 | 作用 |
|------|------|
| `nx` | 统一运维菜单 |
| `nexus-update` | 等同 `deploy/update.sh` |
| `nexus-fresh` | 等同 `install-nexus-fresh.sh` |
| `nexus-install` | 等同 `quick-install.sh` |
> `nx god` 已合并进 `nx` 主菜单,仍可作为兼容别名使用。
---
## 部署脚本一览(`deploy/`
### 1Panel + Docker 生产(主路径)
| 脚本 | 用途 | curl 入口 |
|------|------|-----------|
| [quick-install.sh](deploy/quick-install.sh) | **公共仓库安装入口** | ✅ 上表 |
| [install-1panel-docker.sh](deploy/install-1panel-docker.sh) | 1Panel + Docker + Nexus 全链 | ✅ 上表 |
| [install-nexus-fresh.sh](deploy/install-nexus-fresh.sh) | 全新空库 → 安装向导 | `bash deploy/install-nexus-fresh.sh --skip-clone` |
| [update.sh](deploy/update.sh) | **一键更新**(pull + 重建) | ✅ 上表 |
| [upgrade-1panel-docker.sh](deploy/upgrade-1panel-docker.sh) | 转调 `nexus-1panel.sh upgrade` | — |
| [nexus-1panel.sh](deploy/nexus-1panel.sh) | 安装 / 升级 / check / ports 底层 | — |
| [nx](deploy/nx) | **统一交互菜单** | `nx` |
| [uninstall-mysql-compose.sh](deploy/uninstall-mysql-compose.sh) | 卸载遗留 Compose MySQL 容器 | — |
| [uninstall-redis-compose.sh](deploy/uninstall-redis-compose.sh) | 卸载遗留 Compose Redis 容器 | — |
| [sync-install-wizard-to-container.sh](deploy/sync-install-wizard-to-container.sh) | 热同步 install.html 到容器 | — |
| [download-install-fresh.sh](deploy/download-install-fresh.sh) | 下载安装脚本(私有库可选 Token) | — |
详细说明:[deploy/README-1panel.md](deploy/README-1panel.md)
### 更新选项
```bash
bash deploy/update.sh # 默认:Git 拉取(若有)+ 重建容器
bash deploy/update.sh --check # 仅查 Git
bash deploy/update.sh --no-cache # 无缓存重建镜像
bash deploy/update.sh --no-backup # 跳过 MySQL 备份
```
### Docker Compose 与档位
| 路径 | 用途 |
|------|------|
| [docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) | 生产 Compose**仅 nexus 服务** |
| [docker/profiles/1c4g.env](docker/profiles/1c4g.env) | 1 核 4G |
| [docker/profiles/2c8g.env](docker/profiles/2c8g.env) | 2 核 8G(默认) |
| [docker/profiles/4c16g.env](docker/profiles/4c16g.env) | 4 核 16G |
| [docker/README.md](docker/README.md) | 镜像与本地开发说明 |
### 经典 SSH / Supervisor 部署(宝塔 / 裸机)
| 脚本 | 用途 |
|------|------|
| [install.sh](deploy/install.sh) | 传统安装 |
| [upgrade.sh](deploy/upgrade.sh) | 拉代码 + 重启 |
| [deploy-on-server.sh](deploy/deploy-on-server.sh) | 服务器侧部署 |
| [deploy-production.sh](deploy/deploy-production.sh) | 生产发布流程 |
| [deploy-frontend.sh](deploy/deploy-frontend.sh) | 构建 Vue 前端并打包到 `web/app/` |
| [pre_deploy_check.sh](deploy/pre_deploy_check.sh) | **7 道门控** |
| [health_monitor.sh](deploy/health_monitor.sh) | cron 健康检查 |
| [db_backup.sh](deploy/db_backup.sh) | MySQL 备份 |
| [uninstall.sh](deploy/uninstall.sh) | 卸载 |
---
## 迁机 vs 全新安装
| 场景 | 做法 |
|------|------|
| **新 VPS、空库** | `curl \| quick-install.sh` → 1Panel 自建 MySQL/Redis → `/app/install.html` |
| **已有数据、换机** | `nexus-1panel.sh install --from-env``nx` → 安装向导 |
| **仅升级代码/镜像** | `nx update``bash deploy/update.sh` |
| **不必重装 VPS** | `cd /opt/nexus && nx update` |
每台新服务器自动生成唯一 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`(写入 `docker/.env.prod`,备份 `/root/.nexus-install-secrets-*.env`)。MySQL 密码由自建库时自行设定。
---
## 技术栈与目录
```
Nexus/
├── server/ # FastAPI 后端(Clean Architecture
├── frontend/ # Vue 3 + Vuetify 4 SPA → 构建到 web/app/
├── web/app/ # 静态资源挂载点(构建产物 gitignore)
├── deploy/ # 安装、更新、门控、nx 菜单
├── docker/ # Compose、镜像、profiles
├── docs/ # 设计 / changelog / 审计 / 报告
├── tests/ # API 与集成测试
└── standards/ # 开发与逐行审计标准
```
---
## 开发
```bash
pip install -r requirements.txt
uvicorn server.main:app --reload --port 8600
cd frontend && npm run dev # http://localhost:3000/app/
```
Linux 路径:[docs/project/linux-dev-paths.md](docs/project/linux-dev-paths.md) · [UBUNTU-DEV-README.txt](UBUNTU-DEV-README.txt)
---
## 文档与规范
| 文档 | 说明 |
|------|------|
| [docs/README.md](docs/README.md) | 文档总索引 |
| [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md) | 功能 / API / 14 页 |
| [AGENTS.md](AGENTS.md) · [CLAUDE.md](CLAUDE.md) | Agent 薄入口 |
变更:`docs/changelog/` · 门控:`docs/audit/`
---
## 许可与仓库
| 用途 | 地址 |
|------|------|
| 仓库首页 | http://66.154.115.8:3000/admin/Nexus |
| Git | http://66.154.115.8:3000/admin/Nexus.git |
分支 `main` · 提交前跑 `deploy/pre_deploy_check.sh`7 道门控)
**勿提交**`deploy/nexus-1panel.secrets.sh`、根目录 `.env``SECRETS.md`
+49
View File
@@ -0,0 +1,49 @@
Nexus — Ubuntu 离线开发包
========================
解压:
mkdir -p ~/Nexus && tar xzf nexus-ubuntu-dev-2026-06-03.tar.gz -C ~/
cd ~/Nexus
Python 后端:
sudo apt install -y python3 python3-venv python3-pip git curl build-essential
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
# 复制已有 .env,或执行下方 generate + sync_root_env
MySQL + RedisDocker):
sudo apt install -y docker.io docker-compose-v2
sudo usermod -aG docker "$USER" # 重新登录后免 sudo docker
python3 docker/generate_env.py
python3 docker/sync_root_env.py # 生成根目录 .env127.0.0.1 + 较小连接池)
docker compose up -d mysql redis # Redis 已映射 6379 供宿主机 uvicorn
# 首次空库:先不要 sync_root_env,无 .env 启动 uvicorn 走 /app/install.html 向导
启动 API
bash scripts/start-dev.sh
# 或一键验证:bash scripts/local_verify.sh
前端(Node 20+):
cd frontend && npm ci && npx vite build
编码 / 换行(Windows tar 解压后必做):
git config core.autocrlf false
bash scripts/normalize_worktree_lf.sh
python3 scripts/check_worktree_eol.py
python3 scripts/check_text_eol.py
详见 docs/audit/2026-06-04-encoding-review-ubuntu-phase1.md
SOCKS5 代理(拉 Docker / npm 镜像):
默认 192.168.124.93:10808 — 改 scripts/proxychains-docker.conf 或环境变量
scripts/with-proxy.sh docker compose pull
cd frontend && ../scripts/with-proxy.sh npm ci
source scripts/proxy-env.sh # pip/curl 等
本地管理员(空库一次):
# .env 增加 NEXUS_TEST_ADMIN_PASSWORD=你的密码
python3 scripts/seed_local_admin.py
cd frontend && NEXUS_E2E_BASE=http://127.0.0.1:8600 NEXUS_E2E_PASSWORD=... npm run test:e2e
工作区路径:~/Nexus
路径 SSOTdocs/project/linux-dev-paths.md · docs/project/local-integration-test.md
Git 远程:http://66.154.115.8:3000/admin/Nexus.git(网络恢复后可 git pull
+136
View File
@@ -0,0 +1,136 @@
"""Full codebase audit scanner"""
import sys, os, re
sys.path.insert(0, '.')
# === 1. Backend API endpoints ===
print('=' * 60)
print('1. BACKEND ROUTE REGISTRY')
print('=' * 60)
api_files = [f for f in os.listdir('server/api') if f.endswith('.py') and f not in ('__init__.py', 'dependencies.py', 'schemas.py')]
for f in sorted(api_files):
try:
with open(f'server/api/{f}', 'r', encoding='utf-8') as fh:
content = fh.read()
prefix_match = re.search(r'prefix=["\'](/api/[^"\']+)["\']', content)
prefix = prefix_match.group(1) if prefix_match else '???'
routes = re.findall(r'@router\.(get|post|put|delete|websocket)\(["\'](.*?)["\']', content)
if routes:
print(f' {f}: prefix={prefix}')
for method, path in routes:
print(f' {method.upper():4s} {prefix}{path}')
except Exception as e:
print(f' {f}: ERROR {e}')
# === 2. Frontend API calls ===
print()
print('=' * 60)
print('2. FRONTEND API CALLS')
print('=' * 60)
html_files = [f for f in os.listdir('web/app') if f.endswith('.html')]
for f in sorted(html_files):
try:
with open(f'web/app/{f}', 'r', encoding='utf-8') as fh:
content = fh.read()
pattern = r"API\s*\+\s*['\"](/[^'\"]+)['\"]"
calls = re.findall(pattern, content)
if calls:
print(f' {f}:')
for call in sorted(set(calls)):
print(f' {call}')
except Exception as e:
print(f' {f}: ERROR {e}')
# === 3. Missing imports (AuditLog used but not imported) ===
print()
print('=' * 60)
print('3. POTENTIAL MISSING IMPORTS (AuditLog used without import)')
print('=' * 60)
for f in sorted(api_files):
try:
with open(f'server/api/{f}', 'r', encoding='utf-8') as fh:
content = fh.read()
uses_audit = 'AuditLog(' in content
imports_audit = 'from server.domain.models import' in content and 'AuditLog' in content.split('from server.domain.models import')[1].split('\n')[0] if 'from server.domain.models import' in content else False
# Also check local imports
local_imports_audit = 'from server.domain.models import AuditLog' in content or ('AuditLog' in content and 'import AuditLog' in content)
if uses_audit and not imports_audit and not local_imports_audit:
# Check if it's imported via local import inside function
has_local = 'import AuditLog' in content or 'from server.domain.models import' in content
if not has_local:
print(f' [BUG] {f}: Uses AuditLog() but never imported!')
else:
print(f' [WARN] {f}: Uses AuditLog() via local import only')
except Exception as e:
pass
# === 4. SSH pool usage (acquire without release) ===
print()
print('=' * 60)
print('4. SSH POOL: acquire() without release()')
print('=' * 60)
for root, dirs, files in os.walk('server'):
for f in files:
if not f.endswith('.py'):
continue
path = os.path.join(root, f)
try:
with open(path, 'r', encoding='utf-8') as fh:
content = fh.read()
acquires = len(re.findall(r'ssh_pool\.acquire', content))
releases = len(re.findall(r'ssh_pool\.release', content))
if acquires > 0 and acquires != releases:
print(f' [WARN] {path}: acquire={acquires} release={releases}')
except:
pass
# === 5. AsyncSessionLocal without commit ===
print()
print('=' * 60)
print('5. DB writes without commit()')
print('=' * 60)
for root, dirs, files in os.walk('server'):
for f in files:
if not f.endswith('.py'):
continue
path = os.path.join(root, f)
try:
with open(path, 'r', encoding='utf-8') as fh:
content = fh.read()
has_create = 'repo.create(' in content or 'session.add(' in content or 'session.execute(update' in content
has_commit = 'session.commit()' in content
if has_create and not has_commit and 'api/' in path:
print(f' [WARN] {path}: has DB writes but no session.commit()')
except:
pass
# === 6. Hardcoded secrets or IPs ===
print()
print('=' * 60)
print('6. HARDCODED SECRETS / IPs')
print('=' * 60)
for root, dirs, files in os.walk('server'):
for f in files:
if not f.endswith('.py'):
continue
path = os.path.join(root, f)
try:
with open(path, 'r', encoding='utf-8') as fh:
lines = fh.readlines()
for i, line in enumerate(lines, 1):
if re.search(r'password\s*=\s*["\'][^"\']{3,}["\']', line) and 'test' not in line.lower() and 'hash' not in line.lower() and 'Column' not in line:
print(f' [WARN] {path}:{i}: possible hardcoded password')
if re.search(r'\b10\.0\.\d+\.\d+\b|\b192\.168\.\d+\.\d+\b|\b172\.(1[6-9]|2\d|3[01])\.\d+\.\d+\b', line) and 'example' not in line.lower() and 'comment' not in line.lower():
print(f' [INFO] {path}:{i}: hardcoded private IP')
except:
pass
print()
print('=' * 60)
print('SCAN COMPLETE')
print('=' * 60)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,71 @@
# 1Panel OpenResty — Nexus reverse proxy (paste into site custom config)
# Domain: api.synaglobal.vip
# Upstream: Nexus Docker on 127.0.0.1:8600 (docker-compose.prod.yml)
#
# SSL: configure in 1Panel UI (Let's Encrypt). This snippet assumes HTTPS server block.
upstream nexus_api {
server 127.0.0.1:8600;
keepalive 32;
}
# WebSocket log without query string (JWT must not hit access_log)
log_format nexus_ws '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent';
client_max_body_size 500m;
client_body_timeout 600s;
location /api/ {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 10s;
}
location /ws/ {
access_log /www/sites/api.synaglobal.vip/log/ws.log nexus_ws;
proxy_pass http://nexus_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /health {
proxy_pass http://nexus_api;
proxy_set_header Host $host;
}
location /app/ {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /agent/ {
proxy_pass http://nexus_api;
proxy_set_header Host $host;
}
# Fallback: install mode or misc routes served by FastAPI
location / {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
+257
View File
@@ -0,0 +1,257 @@
# 1Panel + Docker 一键部署
> 完整知识文档:[docs/project/nexus-1panel-operations-knowledge.md](../docs/project/nexus-1panel-operations-knowledge.md)
> 巡检归档(2026-06-06):[docs/project/nexus-god-menu-audit-summary.md](../docs/project/nexus-god-menu-audit-summary.md)
## 脚本速查(公共仓库 · 无需令牌)
| 脚本 | 用途 | 一条命令 |
|------|------|----------|
| `quick-install.sh` | 仅装 Nexus1Panel 已就绪) | `curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" \| bash` |
| `install-1panel-docker.sh` | 重装服务器全链 | `curl -fsSL ".../install-1panel-docker.sh" \| bash` |
| `install-nexus-fresh.sh` | 全新空库安装 | `bash deploy/install-nexus-fresh.sh --skip-clone` |
| **`update.sh`** | **一键更新** | `curl -fsSL ".../update.sh" \| bash` |
| `nexus-1panel.sh` | 底层 install / upgrade | `bash deploy/nexus-1panel.sh upgrade` |
| `fix-1panel-mysql-grant.sh` | 修复 1045 nexus@Docker | `bash deploy/fix-1panel-mysql-grant.sh` |
| `test-1panel-redis.sh` | Redis 认证诊断 | `REDIS_PASS='...' bash deploy/test-1panel-redis.sh` |
| `verify-1panel-install-wizard.sh` | 一键验收(网络/cron/镜像) | `bash deploy/verify-1panel-install-wizard.sh` |
| `install_ops_cron.sh` | 宿主机 cron(巡检+备份) | `sudo bash deploy/install_ops_cron.sh` |
| `mysql_dump_to_file.sh` | MySQL 备份(Docker exec | 见 `db_backup.sh` / `nx` 菜单 `[9]` |
| `upgrade.sh` | 自动识别 Docker/Supervisor | `bash deploy/upgrade.sh` |
| **`nx`** | **统一运维菜单** | `nx` |
档位:`--profile 1c4g` · `2c8g`(默认)· `4c16g`
---
## 重装服务器(1Panel + Docker + Nexus
```bash
curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
```
4 核 16G
```bash
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g
```
已装 1Panel,只装 Nexus
```bash
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
```
## 仅 Nexus Docker
```bash
curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
```
安装完成后:`https://api.synaglobal.vip/app/install.html`
---
## 一键更新(日常)
```bash
cd /opt/nexus && bash deploy/update.sh
# 或
nx update
# 或菜单
nx # → [4] 一键更新
```
| 选项 | 说明 |
|------|------|
| `--check` | 仅查 Git,不重建 |
| `--no-cache` | 无缓存重建镜像(entrypoint 变更后必用) |
| `--no-backup` | 跳过 MySQL 备份 |
| `--prune` | 清理悬空镜像 |
远程:
```bash
curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
curl -fsSL ".../update.sh" | bash -s -- --no-cache
```
---
## NX 全局命令
安装 / 升级 / 首次运行 `nx` 会自动注册到 `/usr/local/bin`
```bash
# 手动注册(任意目录可用 nx
NEXUS_ROOT=/opt/nexus bash /opt/nexus/deploy/install-nx-cli.sh
nx # 统一运维菜单
nexus-update # 等同 update.sh
nexus-fresh # 等同 install-nexus-fresh.sh
nexus-install # 等同 quick-install.sh
```
| 菜单 / 子命令 | 功能 |
|---------------|------|
| `[1]` | 安装向导(档位 / 迁机 / skip-clone |
| `[2]` | 全新安装 → `/app/install.html` |
| `[3]` | 状态 + `/health` + install.html 归档检查 |
| `[4]` / `nx update` | **一键更新**(备份 + 重建镜像 + 升级后验收) |
| `[5]` | 重启 Nexus 容器 |
| `[6]` | 日志 |
| `[7]` | 启动 / 停止 Compose 栈 |
| `[8]` | 无缓存重建镜像 |
| `[9]` | 立即备份 MySQL |
| `[e]` / `nx cron` | 安装 health_monitor + db_backup crontab |
| `[v]` / `nx verify` | 运行 `verify-1panel-install-wizard.sh` |
---
## 三层守护(1Panel Docker
| 层 | 机制 | 说明 |
|----|------|------|
| Layer 1 | Compose `restart` + 容器 `healthcheck` | 容器崩溃自动拉起 |
| Layer 2 | Python `self_monitor`30s | Redis/MySQL/WebSocket 自检 |
| Layer 3 | 宿主机 `health_monitor.sh`cron | `/health` 连续失败 → `docker restart` + 可选 Telegram |
Layer 3 需**在宿主机**安装(容器内无法写 crontab):
```bash
sudo nx cron
# 或
sudo bash /opt/nexus/deploy/install_ops_cron.sh
```
MySQL 定时备份:每日 03:00 → `/var/backups/nexus/``docker exec` 进 1Panel MySQL 容器 dump**无需**宿主机 `mysqldump`)。
---
## 安装完成后
- 向导锁定后 `install.html`**`install.html.bak`**,外网 `/app/install.html` 返回 **404**(正常)
- 登录:`https://你的域名/app/`
- 验收:`sudo nx verify`
---
## MySQL / Redis1Panel 应用商店)
Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安装。
### 1Panel 官方互联方式(必读)
[1Panel 维护者说明](https://github.com/1Panel-dev/1Panel/issues/11676):应用商店应用使用 **bridge + `1panel-network`**;**容器之间用容器名通信**(Docker 内置 DNS),不要用 `localhost` / `127.0.0.1` / `host.docker.internal`
| 场景 | 正确主机 |
|------|----------|
| Nexus → 1Panel MySQL | `1Panel-mysql-xxxx`(容器列表中的名称) |
| Nexus → 1Panel Redis | `1Panel-redis-xxxx` |
| OpenResty → 应用 | `127.0.0.1:端口`OpenResty 为 host 网络) |
`nx update` / `nexus-1panel.sh` 会:
1. 将 Nexus 加入 **`1panel-network`**`docker/docker-compose.1panel.yml`
2. 自动探测 MySQL/Redis 容器名写入 `docker/.env.prod`(含 `NEXUS_REDIS_URL=redis://1Panel-redis-xxxx:6379/0`
3. 安装向导步骤 3 **预填 MySQL/Redis 容器名**
### 安装步骤
1. **应用商店** → 安装 **MySQL**、**Redis**
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`
4. 填写 MySQL 密码;Redis:**用户名通常留空** + 应用参数密码(自动尝试 `:pass@` / `default`
### 常见错误 1045Access denied for 'nexus'@'172.18.x.x'
TCP 已通,但 MySQL 用户多为 **`nexus@localhost`**Docker 内 Nexus 从 **172.x** 连接会被拒绝。
在服务器执行(按提示输入 root 密码与 nexus 密码):
```bash
cd /opt/nexus && bash deploy/fix-1panel-mysql-grant.sh
```
或一次性:
```bash
MYSQL_ROOT_PASSWORD='1Panel里MySQL的root密码' \
NEXUS_DB_PASSWORD='向导里要填的nexus密码' \
bash /opt/nexus/deploy/fix-1panel-mysql-grant.sh
```
然后安装向导步骤 3**库名/用户 `nexus`**,主机为 `1Panel-mysql-xxxx`,密码与上面一致。
若仍报 `Can't connect to MySQL/Redis on 'host.docker.internal'`:说明 Nexus **未接入 1panel-network** 或未探测到容器名,在服务器执行:
```bash
cd /opt/nexus && bash deploy/detect-1panel-services.sh
nx update --no-cache
```
步骤 2 会 TCP 检测 MySQL/Redis;步骤 4 验证账号密码。
卸载旧 Compose 容器:
```bash
bash deploy/uninstall-mysql-compose.sh
bash deploy/uninstall-redis-compose.sh
```
---
## 1Panel 反代
**禁止**脚本改 `/opt/1panel/apps/openresty/`
1. 应用商店 → **OpenResty**
2. 网站 → 反代 `http://127.0.0.1:8600`
3. 参考:`deploy/1panel/openresty-nexus.conf.example`
---
## 密钥
新机自动生成 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY``docker/.env.prod`,备份 `/root/.nexus-install-secrets-*.env`
MySQL 密码在 1Panel 自建库时设定,向导步骤 3 填写。
| 场景 | 命令 |
|------|------|
| 新机 | `curl \| quick-install.sh` |
| 迁机 | `nexus-1panel.sh install --from-env /path/to/.env` |
| 更新 | `update.sh``nx update` |
**Git**:公开仓库 `clone` / `pull` / `curl | bash` 无需令牌。
---
## 安装向导验收(服务器执行)
**1Panel 终端**(root)一键检查网络、容器名预填、cron、镜像版本、本地 `/health`
```bash
cd /opt/nexus && sudo nx verify
```
**未安装**:通过后再打开 `https://你的域名/app/install.html`
**已安装**`install.html` 应 HTTP 404;验收检查 crontab、`mysql_dump_to_file.sh`、容器内 round6/7 代码特性。
常见修复:
```bash
cd /opt/nexus && bash deploy/detect-1panel-services.sh
nx update --no-cache
```
外网 HTTPS 仍失败但脚本本地检查全绿 → 在 1Panel **网站**配置反代 `http://127.0.0.1:8600`(见 `deploy/1panel/openresty-nexus.conf.example`)。
## 安装向导热修复
```bash
cd /opt/nexus && git pull
bash deploy/update.sh --no-cache
# 或仅同步静态:
bash deploy/sync-install-wizard-to-container.sh
```
+272
View File
@@ -0,0 +1,272 @@
#!/usr/bin/env bash
# Apply a Nexus release tarball on the production host.
#
# Usage:
# sudo bash deploy/apply-release-bundle.sh \
# --tarball /tmp/nexus-release-20260708.tar.gz \
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1 \
# --deploy-path /opt/nexus
#
# Validate only (no backup, no rsync, no rebuild):
# bash deploy/apply-release-bundle.sh --validate-only \
# --tarball /tmp/nexus-release-20260708.tar.gz \
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1
#
# This script intentionally preserves runtime secrets and data:
# - .env
# - docker/.env.prod
# - web/data
set -euo pipefail
TARBALL=""
EXPECTED_SHA256=""
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/opt/nexus-backups}"
SKIP_REBUILD=0
DRY_RUN=0
VALIDATE_ONLY=0
APPLY_TMP=""
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
usage() {
sed -n '1,26p' "$0"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--tarball)
TARBALL="${2:-}"
shift 2
;;
--sha256)
EXPECTED_SHA256="${2:-}"
shift 2
;;
--deploy-path)
DEPLOY_PATH="${2:-}"
shift 2
;;
--backup-dir)
BACKUP_DIR="${2:-}"
shift 2
;;
--skip-rebuild)
SKIP_REBUILD=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--validate-only)
VALIDATE_ONLY=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
error "Unknown argument: $1"
usage
exit 2
;;
esac
done
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
error "Missing required command: $1"
exit 127
}
}
require_runtime_cmds() {
require_cmd tar
require_cmd sha256sum
if [[ "$VALIDATE_ONLY" == 1 ]]; then
return 0
fi
require_cmd rsync
require_cmd curl
}
run() {
echo "+ $*"
if [[ "$DRY_RUN" == 1 ]]; then
return 0
fi
"$@"
}
cleanup() {
if [[ -n "${APPLY_TMP:-}" && -d "$APPLY_TMP" ]]; then
rm -rf "$APPLY_TMP"
fi
}
detect_runtime() {
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
echo docker
return
fi
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
echo supervisor
return
fi
echo unknown
}
verify_sha256() {
if [[ -z "$EXPECTED_SHA256" ]]; then
warn "No --sha256 provided; skipping checksum verification"
return 0
fi
local actual
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
error "SHA256 mismatch"
error "expected: $EXPECTED_SHA256"
error "actual: $actual"
exit 1
fi
info "SHA256 OK: $actual"
}
validate_extracted_tree() {
local src="$1"
for required in server deploy Dockerfile.prod frontend-v2 web/app-v2; do
if [[ ! -e "${src}/${required}" ]]; then
error "Release bundle missing required path: ${required}"
exit 1
fi
done
for forbidden in .env docker/.env.prod web/data .venv-py312-codex frontend-v2/node_modules 2025.2 =2025.2; do
if [[ -e "${src}/${forbidden}" ]]; then
error "Release bundle contains forbidden path: ${forbidden}"
exit 1
fi
done
info "Extracted release tree validation OK"
}
backup_current() {
if [[ ! -d "$DEPLOY_PATH" ]]; then
warn "Deploy path does not exist yet: $DEPLOY_PATH"
return 0
fi
local stamp backup
stamp="$(date +%Y%m%d-%H%M%S)"
backup="${BACKUP_DIR}/nexus-before-${stamp}.tar.gz"
run mkdir -p "$BACKUP_DIR"
info "Backup current deploy path -> $backup"
run tar czf "$backup" \
--exclude='./web/data' \
--exclude='./.venv*' \
--exclude='./venv' \
--exclude='./frontend/node_modules' \
--exclude='./frontend-v2/node_modules' \
-C "$(dirname "$DEPLOY_PATH")" "$(basename "$DEPLOY_PATH")"
}
sync_release() {
local src="$1"
run mkdir -p "$DEPLOY_PATH"
info "Sync release tree -> $DEPLOY_PATH"
run rsync -a --delete \
--exclude='.env' \
--exclude='docker/.env.prod' \
--exclude='web/data' \
"${src}/" "${DEPLOY_PATH}/"
}
rebuild_or_restart() {
if [[ "$SKIP_REBUILD" == 1 ]]; then
warn "--skip-rebuild set; not rebuilding/restarting Nexus"
return 0
fi
local runtime
runtime="$(detect_runtime)"
info "Runtime: $runtime"
case "$runtime" in
docker)
if [[ -x "${DEPLOY_PATH}/deploy/nexus-1panel.sh" || -f "${DEPLOY_PATH}/deploy/nexus-1panel.sh" ]]; then
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/nexus-1panel.sh" upgrade --skip-git
else
error "Docker runtime detected but deploy/nexus-1panel.sh missing"
exit 1
fi
if [[ -f "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" ]]; then
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh"
fi
;;
supervisor)
run supervisorctl restart nexus
;;
*)
error "Unknown runtime. Rebuild/restart manually."
exit 1
;;
esac
}
verify_local_endpoints() {
local port health app app_v2
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
port="${port:-8600}"
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
echo " /health -> ${health}"
echo " /app/ -> ${app}"
echo " /app-v2/ -> ${app_v2}"
if [[ "$health" != "ok" || "$app" != "200" || "$app_v2" != "200" ]]; then
error "Local endpoint verification failed"
exit 1
fi
info "Local endpoint verification OK"
}
main() {
if [[ -z "$TARBALL" ]]; then
error "--tarball is required"
usage
exit 2
fi
[[ -f "$TARBALL" ]] || {
error "Tarball not found: $TARBALL"
exit 1
}
require_runtime_cmds
info "Tarball: $TARBALL"
info "Deploy path: $DEPLOY_PATH"
info "Backup dir: $BACKUP_DIR"
verify_sha256
local extracted
APPLY_TMP="$(mktemp -d /tmp/nexus-release-apply.XXXXXX)"
trap cleanup EXIT
run tar xzf "$TARBALL" -C "$APPLY_TMP"
extracted="${APPLY_TMP}/nexus-release"
validate_extracted_tree "$extracted"
if [[ "$VALIDATE_ONLY" == 1 ]]; then
info "Validate-only mode: tarball checksum and content validation passed"
return 0
fi
backup_current
sync_release "$extracted"
rebuild_or_restart
if [[ "$DRY_RUN" != 1 && "$SKIP_REBUILD" != 1 ]]; then
verify_local_endpoints
fi
info "Release applied successfully"
}
main "$@"
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Fail if tracked shell scripts contain CR (CRLF) — breaks bash on Ubuntu.
# Usage: bash deploy/check_shell_eol.sh
# Run from repo root (or set NEXUS_DEPLOY_DIR).
set -euo pipefail
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_REPO_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)"
ROOT="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
cd "${ROOT}"
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "check_shell_eol: not a git repo at ${ROOT}" >&2
exit 1
fi
BAD=0
while IFS= read -r -d '' f; do
if grep -q $'\r' "${f}" 2>/dev/null; then
echo "CRLF/CR detected: ${f}" >&2
BAD=1
fi
done < <(git ls-files -z '*.sh')
if [ "${BAD}" -ne 0 ]; then
echo "Fix: git add --renormalize '*.sh' or run dos2unix on listed files." >&2
exit 1
fi
echo "check_shell_eol: OK (all tracked *.sh are LF-only)"
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Nexus — MySQL 定时备份(Docker 1Panel / 宿主机)
#
# 0 3 * * * NEXUS_ROOT=/opt/nexus /opt/nexus/deploy/db_backup.sh >> /var/log/nexus_backup.log 2>&1
#
# 亦可通过: sudo bash deploy/install_ops_cron.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
NEXUS_ROOT="${NEXUS_ROOT:-$(dirname "$SCRIPT_DIR")}"
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/var/backups/nexus}"
RETENTION_DAYS="${NEXUS_BACKUP_RETENTION_DAYS:-30}"
DUMP_SH="${SCRIPT_DIR}/mysql_dump_to_file.sh"
if [[ ! -f "$DUMP_SH" ]]; then
echo "[$(date)] ERROR: missing $DUMP_SH"
exit 1
fi
chmod +x "$DUMP_SH" 2>/dev/null || true
mkdir -p "$BACKUP_DIR"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
RAW_FILE="${BACKUP_DIR}/nexus_daily_${TIMESTAMP}.sql"
BACKUP_FILE="${RAW_FILE}.gz"
echo "[$(date)] Starting backup → $BACKUP_FILE"
if ! NEXUS_ROOT="$NEXUS_ROOT" bash "$DUMP_SH" --output "$RAW_FILE" --root "$NEXUS_ROOT"; then
echo "[$(date)] ERROR: mysqldump failed"
rm -f "$RAW_FILE"
exit 1
fi
gzip -f "$RAW_FILE"
if [[ ! -f "$BACKUP_FILE" ]]; then
echo "[$(date)] ERROR: backup file not created"
exit 1
fi
SIZE="$(du -h "$BACKUP_FILE" | cut -f1)"
echo "[$(date)] Backup completed: $BACKUP_FILE ($SIZE)"
DELETED="$(find "$BACKUP_DIR" -name 'nexus_*.sql.gz' -mtime +"${RETENTION_DAYS}" -delete -print 2>/dev/null | wc -l)"
if [[ "$DELETED" -gt 0 ]]; then
echo "[$(date)] Cleaned up $DELETED old backup(s) (>${RETENTION_DAYS} days)"
fi
echo "[$(date)] Backup process complete"
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# deploy-frontend.sh — Build frontend v1 + app-v2 and deploy to server
#
# Docker 生产:前端在镜像内构建,远程执行 nexus-1panel.sh upgrade
# Supervisortar 上传到 web/app + web/app-v2 + supervisorctl restart
#
# Usage: bash deploy/deploy-frontend.sh
set -euo pipefail
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
fi
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
echo "═══ Nexus Frontend Deploy ═══"
REMOTE_RUNTIME=$(ssh_cmd 'bash -s' <<'REMOTE'
set -euo pipefail
docker_cmd() {
if docker "$@" 2>/dev/null; then return 0; fi
sudo docker "$@" 2>/dev/null
}
compose_cmd() {
if docker compose "$@" 2>/dev/null; then return 0; fi
sudo docker compose "$@" 2>/dev/null
}
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
[[ -d "$d/server" ]] || continue
if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then
prof="$d/docker/.host-profile"
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
if [[ -f "$prof" ]]; then
args=(-f "$d/docker/docker-compose.prod.yml")
docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
args+=(-f "$d/docker/docker-compose.1panel.yml")
q=$(cd "$d" && compose_cmd "${args[@]}" \
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
[[ -n "$q" ]] && echo "docker $d" && exit 0
fi
fi
if [[ -f "$d/deploy/nexus-1panel.sh" ]]; then
q=$(docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E '^nexus-prod-nexus-' || true)
[[ -n "$q" ]] && echo "docker $d" && exit 0
fi
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
echo "supervisor $d"
exit 0
fi
done
echo "unknown"
REMOTE
)
RUNTIME="${REMOTE_RUNTIME%% *}"
DEPLOY_PATH="${REMOTE_RUNTIME#* }"
[[ "$DEPLOY_PATH" == "$REMOTE_RUNTIME" ]] && DEPLOY_PATH="/www/wwwroot/api.synaglobal.vip"
echo "Remote runtime: ${RUNTIME} @ ${DEPLOY_PATH}"
echo "▶ Building frontend v1 (local verify)..."
cd frontend && npx vite build && cd ..
if [[ -d frontend-v2 ]]; then
echo "▶ Building frontend v2 (/app-v2, local verify)..."
(cd frontend-v2 && npm run build:safe)
test -f web/app-v2/index.html
fi
echo "✓ Build complete"
if [[ "$RUNTIME" == "docker" ]]; then
echo "▶ Docker 生产:重建镜像以包含新前端(非 tar 热更新)..."
ssh_cmd "cd ${DEPLOY_PATH} && sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
else
echo "▶ Packaging web/app and web/app-v2..."
pkg_dir="$(mktemp -d)"
trap 'rm -rf "${pkg_dir}"' EXIT
mkdir -p "${pkg_dir}/app"
cp -a web/app/index.html web/app/assets "${pkg_dir}/app/"
if [[ -f web/app-v2/index.html && -d web/app-v2/assets ]]; then
mkdir -p "${pkg_dir}/app-v2"
cp -a web/app-v2/index.html web/app-v2/assets "${pkg_dir}/app-v2/"
fi
tar czf /tmp/nexus-frontend.tar.gz -C "${pkg_dir}" .
echo "✓ Package ready ($(du -h /tmp/nexus-frontend.tar.gz | cut -f1))"
echo "▶ Uploading to server..."
scp "${SSH_OPTS[@]}" /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
echo "✓ Upload complete"
echo "▶ Deploying on server..."
ssh_cmd "set -e; mkdir -p ${DEPLOY_PATH}/web; tar xzf /tmp/nexus-frontend.tar.gz -C ${DEPLOY_PATH}/web; rm /tmp/nexus-frontend.tar.gz"
echo "▶ Pruning orphaned assets..."
if [[ -f deploy/prune_frontend_assets.py ]]; then
if ! ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app --dry-run"; then
echo "✗ Prune dry-run failed"
exit 1
fi
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
fi
echo "✓ Extracted and pruned"
echo "▶ Restarting backend..."
ssh_cmd "supervisorctl restart nexus"
echo "✓ Restarted"
fi
sleep 3
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:8600/health" 2>/dev/null || echo "000")
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/" 2>/dev/null || echo "000")
SPA_V2=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app-v2/" 2>/dev/null || echo "000")
echo ""
echo "═══ Verification ═══"
echo " /health → $HEALTH"
echo " /app/ → $SPA"
echo " /app-v2/ → $SPA_V2"
if [[ "$HEALTH" == "200" || "$HEALTH" == "ok" ]] && [[ "$SPA" == "200" ]] && [[ "$SPA_V2" == "200" ]]; then
echo "✓ Deploy successful!"
else
echo "✗ Verification failed — check server logs"
exit 1
fi
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Run ON production server — 自动识别 Docker / Supervisor
#
# Usage (on server):
# cd /opt/nexus && sudo bash deploy/deploy-on-server.sh
# cd /www/wwwroot/api.synaglobal.vip && bash deploy/deploy-on-server.sh
#
# Optional env:
# NEXUS_GITEA_TOKEN — if git pull needs auth
# NEXUS_GIT_REMOTE — override authenticated remote, default: https://axs.kuma1xn.vip/admin/Nexus.git
# SKIP_FRONTEND=1 — Supervisor 模式跳过热更新前端 tar
# NEXUS_DEPLOY_PATH — 覆盖自动检测路径
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./detect_deploy_runtime.sh
source "${SCRIPT_DIR}/detect_deploy_runtime.sh"
DEPLOY_PATH="$(resolve_nexus_root "${NEXUS_DEPLOY_PATH:-}" 2>/dev/null || true)"
if [[ -z "$DEPLOY_PATH" ]]; then
echo "ERROR: cannot resolve Nexus root" >&2
exit 1
fi
cd "${DEPLOY_PATH}"
RUNTIME="$(detect_nexus_runtime "$DEPLOY_PATH")"
PORT="$(nexus_publish_port_for "$DEPLOY_PATH")"
echo "═══ Nexus server-side deploy ═══"
echo "Path: ${DEPLOY_PATH}"
echo "Runtime: ${RUNTIME}"
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
GIT_REMOTE="${NEXUS_GIT_REMOTE:-https://axs.kuma1xn.vip/admin/Nexus.git}"
GIT_REMOTE_AUTH="${GIT_REMOTE/https:\/\//https://admin:${NEXUS_GITEA_TOKEN}@}"
git remote set-url origin "${GIT_REMOTE_AUTH}"
fi
if [[ "$RUNTIME" == "docker" ]]; then
echo "▶ Docker upgrade (pull + rebuild)..."
exec env NEXUS_ROOT="$DEPLOY_PATH" bash "$DEPLOY_PATH/deploy/nexus-1panel.sh" upgrade
fi
echo "▶ git fetch + reset --hard origin/main..."
git fetch --all
git reset --hard origin/main
echo " HEAD: $(git log -1 --oneline)"
echo "▶ supervisorctl restart nexus..."
supervisorctl restart nexus
if [[ -f /tmp/nexus-frontend.tar.gz && "${SKIP_FRONTEND:-0}" != "1" ]]; then
echo "▶ extract /tmp/nexus-frontend.tar.gz..."
mkdir -p web/app web/app-v2
if tar tzf /tmp/nexus-frontend.tar.gz | grep -Eq '^(\./)?app/'; then
tar xzf /tmp/nexus-frontend.tar.gz -C web
else
# Backward compatibility for legacy archives containing index.html + assets/ at root.
tar xzf /tmp/nexus-frontend.tar.gz -C web/app --exclude=app-v2
if tar tzf /tmp/nexus-frontend.tar.gz | grep -Eq '^(\./)?app-v2/'; then
tar xzf /tmp/nexus-frontend.tar.gz -C web/app-v2 --strip-components=1 app-v2
fi
fi
rm -f /tmp/nexus-frontend.tar.gz
if [[ -f deploy/prune_frontend_assets.py ]]; then
python3 deploy/prune_frontend_assets.py web/app --dry-run
python3 deploy/prune_frontend_assets.py web/app
fi
supervisorctl restart nexus
fi
sleep 3
HEALTH=$(curl -s "http://127.0.0.1:${PORT}/health" || true)
SPA=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/app/" || echo "000")
SPA_V2=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/app-v2/" || echo "000")
echo " /health → ${HEALTH}"
echo " /app/ → ${SPA}"
echo " /app-v2/ → ${SPA_V2}"
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" && "${SPA_V2}" == "200" ]]; then
echo "✓ Server deploy OK"
suggest_ops_cron "$DEPLOY_PATH"
else
echo "✗ Verify failed — check: supervisorctl status nexus" >&2
exit 1
fi
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env bash
# Full production deploy: local rsync + backend upgrade + frontend + health
#
# 默认从本机工作区 rsync 到生产机,不 push Gitea、不远程 git pull。
#
# Prerequisites on YOUR machine:
# - 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
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}" "$@"; }
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
echo " Configure ~/.ssh/config (Host nexus) or set NEXUS_SSH / NEXUS_SSH_KEY" >&2
exit 1
fi
REMOTE_DETECT=$(ssh_cmd 'bash -s' <<'REMOTE'
set -euo pipefail
docker_cmd() {
if docker "$@" 2>/dev/null; then return 0; fi
sudo docker "$@" 2>/dev/null
}
compose_cmd() {
if docker compose "$@" 2>/dev/null; then return 0; fi
sudo docker compose "$@" 2>/dev/null
}
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
[[ -d "$d/server" ]] || continue
echo "ROOT=$d"
if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then
prof="$d/docker/.host-profile"
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
if [[ -f "$prof" ]]; then
args=(-f "$d/docker/docker-compose.prod.yml")
docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
args+=(-f "$d/docker/docker-compose.1panel.yml")
q=$(cd "$d" && compose_cmd "${args[@]}" \
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
[[ -n "$q" ]] && echo RUNTIME=docker && exit 0
fi
fi
if [[ -f "$d/deploy/nexus-1panel.sh" ]] && command -v nx >/dev/null 2>&1; then
q=$(docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E '^nexus-prod-nexus-' || true)
[[ -n "$q" ]] && echo RUNTIME=docker && exit 0
fi
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
echo RUNTIME=supervisor
exit 0
fi
echo RUNTIME=unknown
exit 0
done
echo RUNTIME=none
REMOTE
)
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-$(echo "$REMOTE_DETECT" | sed -n 's/^ROOT=//p' | head -1)}"
RUNTIME="$(echo "$REMOTE_DETECT" | sed -n 's/^RUNTIME=//p' | head -1)"
if [[ -z "$DEPLOY_PATH" ]]; then
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
fi
if [[ -z "$RUNTIME" || "$RUNTIME" == "none" ]]; then
RUNTIME="unknown"
fi
echo "Deploy path: ${DEPLOY_PATH}"
echo "Runtime: ${RUNTIME}"
resolve_deploy_chown() {
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
echo "${NEXUS_DEPLOY_CHOWN}"
return
fi
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
echo "www:www"
return
fi
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
}
DEPLOY_CHOWN="$(resolve_deploy_chown)"
echo "File owner: ${DEPLOY_CHOWN}"
if [[ "$DEPLOY_VIA_GIT" == 1 ]]; then
echo "▶ Git mode: remote git pull..."
if [[ "$RUNTIME" == "docker" ]]; then
ssh_cmd "cd ${DEPLOY_PATH} && sudo git fetch --all && sudo git reset --hard origin/main && \
sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
elif [[ "$RUNTIME" == "supervisor" ]]; then
ssh_cmd "cd ${DEPLOY_PATH} && git fetch --all && git reset --hard origin/main && supervisorctl restart nexus"
else
echo "ERROR: 无法识别远程运行时" >&2
exit 1
fi
else
echo "▶ Local sync → ${DEPLOY_PATH}..."
NEXUS_DEPLOY_PATH="${DEPLOY_PATH}" bash "${ROOT}/deploy/rsync-local-to-server.sh"
if [[ "$RUNTIME" == "docker" ]]; then
echo "▶ Docker: 重建镜像(--skip-git..."
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade --skip-git"
elif [[ "$RUNTIME" == "supervisor" ]]; then
echo "▶ Supervisor: restart..."
ssh_cmd "supervisorctl restart nexus"
else
echo "ERROR: 无法识别远程运行时(非 Docker 也非 Supervisor" >&2
echo " 请手动: ssh ${NEXUS_SSH_HOST} 'sudo nx update'" >&2
exit 1
fi
fi
if [[ "$RUNTIME" == "docker" || "$RUNTIME" == "supervisor" ]]; then
echo "▶ Frontend: local vite build + upload to host web/app and web/app-v2..."
cd frontend && npx vite build && cd ..
if [[ -d frontend-v2 ]]; then
(cd frontend-v2 && npm run build:safe)
test -f web/app-v2/index.html
fi
pkg_dir="$(mktemp -d)"
trap 'rm -rf "${pkg_dir}"' EXIT
mkdir -p "${pkg_dir}/app"
cp -a web/app/index.html web/app/assets "${pkg_dir}/app/"
if [[ -f web/app-v2/index.html && -d web/app-v2/assets ]]; then
mkdir -p "${pkg_dir}/app-v2"
cp -a web/app-v2/index.html web/app-v2/assets "${pkg_dir}/app-v2/"
fi
tar czf /tmp/nexus-frontend.tar.gz -C "${pkg_dir}" .
scp_cmd /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
ssh_cmd "set -e; sudo mkdir -p ${DEPLOY_PATH}/web; sudo tar xzf /tmp/nexus-frontend.tar.gz -C ${DEPLOY_PATH}/web; rm /tmp/nexus-frontend.tar.gz; sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app ${DEPLOY_PATH}/web/app-v2"
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app --dry-run"
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
fi
fi
if [[ "$RUNTIME" == "docker" ]]; then
echo "▶ Sync host web/app and web/app-v2 into container..."
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} NEXUS_PUBLISH_PORT=\$(grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod 2>/dev/null | cut -d= -f2 | tr -d '\"' || echo 8600) bash ${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" || {
echo "WARN: web/app + web/app-v2 sync failed — run sync_webapp_to_container.sh on server manually" >&2
}
fi
sleep 3
PORT="$(ssh_cmd "grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod 2>/dev/null | cut -d= -f2 | tr -d '\"'" 2>/dev/null || true)"
PORT="${PORT:-8600}"
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:${PORT}/health" 2>/dev/null || true)
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app/" 2>/dev/null || echo "000")
SPA_V2=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app-v2/" 2>/dev/null || echo "000")
echo " /health → ${HEALTH}"
echo " /app/ → ${SPA}"
echo " /app-v2/ → ${SPA_V2}"
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" && "${SPA_V2}" == "200" ]]; then
echo "✓ Deploy successful"
if ssh_cmd "command -v crontab >/dev/null && crontab -l 2>/dev/null | grep -q nexus-health-monitor" 2>/dev/null; then
echo "✓ Ops cron (health_monitor) already installed"
else
echo "▶ Installing ops cron (health_monitor + db_backup)…"
if ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/install_ops_cron.sh" 2>/dev/null; then
echo "✓ Ops cron installed"
else
echo " 提示: 远程 cron 安装失败,可在服务器执行: sudo nx cron"
fi
fi
else
echo "✗ Verification failed — check logs on server" >&2
exit 1
fi
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Detect 1Panel App Store MySQL/Redis container names on 1panel-network.
# See: https://github.com/1Panel-dev/1Panel/issues/11676
# https://github.com/1Panel-dev/appstore (mysql/redis → networks: 1panel-network)
set -euo pipefail
network_container_names() {
docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null \
| tr ' ' '\n' | sed '/^$/d'
}
pick_service_container() {
local kind="$1"
local names line
names="$(network_container_names)"
if [[ -z "$names" ]]; then
docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "(^1Panel-${kind}-|${kind})" | head -1 || true
return
fi
line="$(echo "$names" | grep -iE "^1Panel-${kind}-" | head -1 || true)"
if [[ -n "$line" ]]; then
echo "$line"
return
fi
echo "$names" | grep -i "$kind" | head -1 || true
}
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
echo "1panel-network not found" >&2
exit 1
fi
MYSQL_HOST="$(pick_service_container mysql)"
REDIS_HOST="$(pick_service_container redis)"
if [[ -z "$MYSQL_HOST" && -z "$REDIS_HOST" ]]; then
echo "No MySQL/Redis containers on 1panel-network" >&2
exit 2
fi
[[ -n "$MYSQL_HOST" ]] && echo "NEXUS_1PANEL_DB_HOST=${MYSQL_HOST}"
[[ -n "$REDIS_HOST" ]] && echo "NEXUS_1PANEL_REDIS_HOST=${REDIS_HOST}"
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env bash
# Nexus — 部署运行时检测(Docker 1Panel / Supervisor 裸机)
#
# source deploy/detect_deploy_runtime.sh
# ROOT="$(resolve_nexus_root)"
# RUNTIME="$(detect_nexus_runtime "$ROOT")" # docker | supervisor | unknown
set -euo pipefail
_COMPOSE_FILE="docker/docker-compose.prod.yml"
_COMPOSE_1PANEL="docker/docker-compose.1panel.yml"
_ENV_PROD="docker/.env.prod"
resolve_nexus_root() {
local hint="${1:-}"
if [[ -n "$hint" && -d "$hint/server" ]]; then
echo "$hint"
return 0
fi
if [[ -n "${NEXUS_ROOT:-}" && -d "${NEXUS_ROOT}/server" ]]; then
echo "$NEXUS_ROOT"
return 0
fi
if [[ -n "${NEXUS_DEPLOY_PATH:-}" && -d "${NEXUS_DEPLOY_PATH}/server" ]]; then
echo "$NEXUS_DEPLOY_PATH"
return 0
fi
local d
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
if [[ -d "$d/server" ]]; then
echo "$d"
return 0
fi
done
for d in /www/wwwroot/*/; do
[[ -d "${d}server" ]] || continue
echo "${d%/}"
return 0
done
if command -v supervisorctl >/dev/null 2>&1; then
local cwd
cwd="$(supervisorctl status nexus 2>/dev/null | grep -oP 'cwd=\K[^ ,]+' || true)"
if [[ -n "$cwd" && -d "$cwd/server" ]]; then
echo "$cwd"
return 0
fi
fi
return 1
}
_has_1panel_network() {
docker network inspect 1panel-network >/dev/null 2>&1
}
detect_nexus_runtime() {
local root="$1"
if [[ -f "$root/$_ENV_PROD" ]] && command -v docker >/dev/null 2>&1 \
&& docker compose version >/dev/null 2>&1; then
local -a args=(-f "$root/$_COMPOSE_FILE")
if _has_1panel_network && [[ -f "$root/$_COMPOSE_1PANEL" ]]; then
args+=(-f "$root/$_COMPOSE_1PANEL")
fi
local prof q
prof="$root/docker/.host-profile"
[[ -f "$prof" ]] || prof="$root/docker/profiles/2c8g.env"
if [[ -f "$prof" ]]; then
q="$(cd "$root" && docker compose "${args[@]}" \
--env-file "$root/$_ENV_PROD" --env-file "$prof" \
ps -q nexus 2>/dev/null || true)"
if [[ -n "$q" ]]; then
echo docker
return 0
fi
fi
fi
if command -v supervisorctl >/dev/null 2>&1 \
&& supervisorctl status nexus >/dev/null 2>&1; then
echo supervisor
return 0
fi
echo unknown
}
nexus_publish_port_for() {
local root="$1"
local port
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$root/$_ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
if [[ -z "$port" && -f "$root/.env" ]]; then
port="$(grep -E '^NEXUS_PORT=' "$root/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
fi
echo "${port:-8600}"
}
nexus_restart_service() {
local root="$1"
local runtime
runtime="$(detect_nexus_runtime "$root")"
case "$runtime" in
docker)
local cname
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
if [[ -n "$cname" ]]; then
docker restart "$cname"
return 0
fi
return 1
;;
supervisor)
supervisorctl restart nexus
;;
*)
return 1
;;
esac
}
nexus_has_ops_cron() {
command -v crontab >/dev/null 2>&1 \
&& crontab -l 2>/dev/null | grep -q 'nexus-health-monitor'
}
suggest_ops_cron() {
local root="$1"
if nexus_has_ops_cron; then
return 0
fi
echo ""
echo "[WARN] 未检测到 health_monitor / db_backup cron"
echo " 建议: sudo bash ${root}/deploy/install_ops_cron.sh"
echo " 或: sudo nx cron"
}
prompt_ops_cron_if_missing() {
local root="$1"
if nexus_has_ops_cron; then
echo "[INFO] Crontab 已含 health_monitor / db_backup"
return 0
fi
suggest_ops_cron "$root"
if [[ ! -t 0 ]]; then
return 0
fi
local ans
read -r -p "现在安装巡检/备份 cron? [Y/n]: " ans
if [[ "$ans" == "n" || "$ans" == "N" ]]; then
echo "[WARN] 已跳过 cron 安装"
return 0
fi
bash "${root}/deploy/install_ops_cron.sh"
}
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# 下载并执行 install-nexus-fresh.sh(公共仓库无需令牌)
#
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/download-install-fresh.sh" | bash
#
# 推荐直接使用:
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
set -euo pipefail
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
OUT="${OUT:-/tmp/install-nexus-fresh.sh}"
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
done
download() {
local url base
for base in "${NEXUS_RAW_BASE}" "${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
url="${base}/deploy/install-nexus-fresh.sh"
if curl -fsSL "$url" -o "$OUT" 2>/dev/null && head -1 "$OUT" | grep -q '^#!/'; then
ok "已下载 install-nexus-fresh.sh"
chmod +x "$OUT"
exec bash "$OUT" "$@"
fi
done
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
url="${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/api/v1/repos/admin/Nexus/raw/deploy/install-nexus-fresh.sh?ref=${NEXUS_GIT_BRANCH}"
if curl -fsSL -H "Authorization: token ${NEXUS_GITEA_TOKEN}" "$url" -o "$OUT" 2>/dev/null \
&& head -1 "$OUT" | grep -q '^#!/'; then
ok "已下载(API + 令牌)"
chmod +x "$OUT"
exec bash "$OUT" "$@"
fi
fi
return 1
}
if ! download "$@"; then
err "下载失败,请改用公共仓库一键入口:"
err " curl -fsSL \"${NEXUS_RAW_BASE}/deploy/quick-install.sh\" | bash"
exit 1
fi
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Grant nexus@'%' on 1Panel App Store MySQL for Docker (1panel-network) install wizard.
# Fixes: Access denied for user 'nexus'@'172.18.x.x' (1045)
#
# Usage:
# MYSQL_ROOT_PASSWORD='root密码' NEXUS_DB_PASSWORD='nexus密码' bash deploy/fix-1panel-mysql-grant.sh
# bash deploy/fix-1panel-mysql-grant.sh # prompts
set -euo pipefail
NEXUS_DB_NAME="${NEXUS_DB_NAME:-nexus}"
NEXUS_DB_USER="${NEXUS_DB_USER:-nexus}"
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; exit 1; }
pick_mysql_container() {
local name
if [[ -n "${MYSQL_CONTAINER:-}" ]]; then
echo "$MYSQL_CONTAINER"
return 0
fi
if docker network inspect 1panel-network >/dev/null 2>&1; then
name="$(docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' \
| tr ' ' '\n' | grep -iE '^1Panel-mysql-' | head -1 || true)"
[[ -n "$name" ]] && echo "$name" && return 0
fi
name="$(docker ps --format '{{.Names}}' | grep -iE '^1Panel-mysql-' | head -1 || true)"
[[ -n "$name" ]] && echo "$name" && return 0
error "未找到 1Panel MySQL 容器(可设 MYSQL_CONTAINER=名称)"
}
prompt_secret() {
local var="$1" prompt="$2"
if [[ -n "${!var:-}" ]]; then
return 0
fi
read -r -s -p "$prompt: " "$var"
echo ""
if [[ -z "${!var:-}" ]]; then
error "$var 不能为空"
fi
}
main() {
[[ "${EUID:-$(id -u)}" -eq 0 ]] || error "请用 root 运行"
local mysql_c
mysql_c="$(pick_mysql_container)"
info "MySQL 容器: $mysql_c"
prompt_secret MYSQL_ROOT_PASSWORD "1Panel MySQL root 密码"
prompt_secret NEXUS_DB_PASSWORD "nexus 用户密码(与安装向导步骤 3 一致)"
info "创建库/用户并授权 ${NEXUS_DB_USER}@'%' → ${NEXUS_DB_NAME}.* ..."
docker exec -i "$mysql_c" mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" <<SQL
CREATE DATABASE IF NOT EXISTS \`${NEXUS_DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
ALTER USER '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
GRANT ALL PRIVILEGES ON \`${NEXUS_DB_NAME}\`.* TO '${NEXUS_DB_USER}'@'%';
FLUSH PRIVILEGES;
SELECT user, host FROM mysql.user WHERE user = '${NEXUS_DB_USER}';
SQL
info "完成。请在安装向导步骤 3 使用: 库/用户=${NEXUS_DB_NAME}/${NEXUS_DB_USER},密码与上面一致"
info "若已改 install.py,执行: bash deploy/sync-install-wizard-to-container.sh"
}
main "$@"
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Gate Log — 门控执行日志记录器
每次门控检查的结果都追加到 deploy/gate_log.jsonl (JSON Lines)
每行一条记录,包含:时间戳、门控名、结果、详情
这个文件是用户审查AI是否偷懒的关键证据:
- 查看最近一次: tail -1 deploy/gate_log.jsonl | python -m json.tool
- 查看所有BLOCK: grep '"BLOCK"' deploy/gate_log.jsonl
- 查看某天: grep '2026-05-26' deploy/gate_log.jsonl
"""
import json
import os
from datetime import datetime, timezone
GATE_LOG = os.path.join(
os.environ.get("NEXUS_DEPLOY_DIR", "/opt/nexus"),
"deploy", "gate_log.jsonl"
)
def log_gate(gate_name: str, result: str, detail: str = "", files_checked: list = None):
"""Record a gate check result.
Args:
gate_name: e.g. "Changelog", "Audit", "Test", "Lint", "Import", "Security", "Review"
result: "PASS", "BLOCK", "SKIP", "WARN"
detail: Human-readable detail
files_checked: List of files involved in this gate check
"""
entry = {
"ts": datetime.now(timezone.utc).isoformat(),
"gate": gate_name,
"result": result,
"detail": detail,
}
if files_checked:
entry["files"] = files_checked
os.makedirs(os.path.dirname(GATE_LOG), exist_ok=True)
with open(GATE_LOG, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
if __name__ == "__main__":
import sys
if len(sys.argv) >= 3:
log_gate(sys.argv[1], sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else "")
else:
# Print recent log
if os.path.exists(GATE_LOG):
with open(GATE_LOG, encoding="utf-8") as f:
lines = f.readlines()[-20:]
for line in lines:
d = json.loads(line)
icon = {"PASS": "", "BLOCK": "🚫", "SKIP": "⏭️", "WARN": "⚠️"}.get(d["result"], "?")
print(f"{d['ts'][:19]} {icon} {d['gate']}: {d.get('detail','')}")
else:
print("No gate log found")
+317 -56
View File
@@ -1,70 +1,331 @@
#!/bin/bash
#!/usr/bin/env bash
# Nexus — External Health Monitor (Layer 3 of 3-tier guardian)
# Runs via crontab every minute.
# Does NOT depend on Python — even if Python completely crashes, this script works.
# Runs via crontab every minute. Works on Docker (1Panel) and Supervisor bare-metal.
#
# Guardian layers:
# Layer 1: Supervisor — auto-restart crashed Python process
# Layer 2: Python self_monitor — internal Redis/MySQL/WS checks (30s)
# Layer 3: Shell health_monitor.sh — external HTTP health check (1min cron)
#
# Flow:
# 1. HTTP GET http://127.0.0.1:8600/health
# 2. If fails >= 3 consecutive times → restart via supervisorctl + Telegram alert
# 3. If restart succeeds → Telegram recovery notification
# 4. If restart fails → Telegram "needs human intervention" alert
#
# *** Configure DEPLOY_DIR and NEXUS_SERVICE before deploying ***
# Layer 1: Docker restart policy / Supervisor autorestart
# Layer 2: Python self_monitor (30s)
# Layer 3: This script — HTTP /health, 3 failures → restart + optional Telegram
set -euo pipefail
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-/opt/nexus}"
NEXUS_SERVICE="${NEXUS_SERVICE:-nexus}"
FAIL_FILE="/tmp/nexus_health_fail_count"
STATE_DIR=""
FAIL_FILE=""
LAST_RESTART_FILE=""
# Legacy paths (cron user may have created root-owned files under /tmp)
LEGACY_FAIL_FILE="/tmp/nexus_health_fail_count"
LEGACY_LAST_RESTART_FILE="/tmp/nexus_health_last_restart"
MAX_FAIL=3
HEALTH_URL="http://127.0.0.1:8600/health"
RESTART_COOLDOWN_SEC="${NEXUS_HEALTH_RESTART_COOLDOWN:-300}"
ENV_PROD="${DEPLOY_DIR}/docker/.env.prod"
# ── Read Telegram config from .env file ──
DOTENV="$DEPLOY_DIR/.env"
BOT_TOKEN=""
CHAT_ID=""
if [ -f "$DOTENV" ]; then
BOT_TOKEN=$(grep '^NEXUS_TELEGRAM_BOT_TOKEN=' "$DOTENV" 2>/dev/null | cut -d'=' -f2- || echo "")
CHAT_ID=$(grep '^NEXUS_TELEGRAM_CHAT_ID=' "$DOTENV" 2>/dev/null | cut -d'=' -f2- || echo "")
fi
send_telegram() {
[ -z "$BOT_TOKEN" ] || [ -z "$CHAT_ID" ] && return 1
curl -sf --max-time 10 "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d chat_id="$CHAT_ID" \
-d text="$1" \
-d parse_mode=HTML \
> /dev/null 2>&1
ensure_state_paths() {
if [[ -n "$STATE_DIR" ]]; then
return 0
fi
local preferred="${DEPLOY_DIR}/var/layer3-health"
local fallback="${HOME:-/tmp}/.nexus/layer3-health"
if mkdir -p "$preferred" 2>/dev/null && [[ -w "$preferred" ]]; then
STATE_DIR="$preferred"
elif command -v sudo >/dev/null 2>&1 && sudo mkdir -p "$preferred" 2>/dev/null \
&& sudo chown -R "$(id -un):$(id -gn)" "$preferred" 2>/dev/null \
&& [[ -w "$preferred" ]]; then
STATE_DIR="$preferred"
else
mkdir -p "$fallback"
STATE_DIR="$fallback"
fi
FAIL_FILE="${STATE_DIR}/fail_count"
LAST_RESTART_FILE="${STATE_DIR}/last_restart"
}
# ── Health Check ──
if ! curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
# Python is NOT responding
FAIL=$(cat "$FAIL_FILE" 2>/dev/null || echo "0")
FAIL=$((FAIL + 1))
echo "$FAIL" > "$FAIL_FILE"
docker_cmd() {
if docker "$@" 2>/dev/null; then
return 0
fi
if command -v sudo >/dev/null 2>&1; then
sudo docker "$@" 2>/dev/null
else
return 1
fi
}
if [ "$FAIL" -ge "$MAX_FAIL" ]; then
# Consecutive failures >= threshold → attempt restart
send_telegram "🔴 <b>Nexus后端连续${MAX_FAIL}次无响应</b>\n正在通过Supervisor自动重启..."
supervisorctl restart "$NEXUS_SERVICE" > /dev/null 2>&1
sleep 5
init_layer3_state() {
ensure_state_paths
chmod 755 "$STATE_DIR" 2>/dev/null || true
if [[ ! -f "$FAIL_FILE" && -f "$LEGACY_FAIL_FILE" ]]; then
cp "$LEGACY_FAIL_FILE" "$FAIL_FILE" 2>/dev/null || true
fi
if [[ ! -f "$LAST_RESTART_FILE" && -f "$LEGACY_LAST_RESTART_FILE" ]]; then
cp "$LEGACY_LAST_RESTART_FILE" "$LAST_RESTART_FILE" 2>/dev/null || true
fi
}
# Verify restart
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
send_telegram "🟢 <b>Nexus后端已恢复</b>\nSupervisor重启成功"
echo "0" > "$FAIL_FILE"
else
send_telegram "🔴 <b>Nexus重启后仍未恢复!请人工介入</b>\nSupervisor重启失败,需要手动检查"
# Don't reset fail count — keep retrying each cron cycle
fi
write_state_file() {
local path="$1" value="$2"
init_layer3_state
printf '%s\n' "$value" >"$path" 2>/dev/null || return 1
}
resolve_publish_port() {
local port
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
echo "${port:-8600}"
}
PUBLISH_PORT="$(resolve_publish_port)"
HEALTH_URL="http://127.0.0.1:${PUBLISH_PORT}/health"
resolve_nexus_container() {
docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
}
read_env_var() {
local key="$1" file="$2"
grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"' || true
}
normalize_env_value() {
local v="${1:-}"
v="${v//$'\r'/}"
if [[ ${#v} -ge 2 && ${v:0:1} == '"' && ${v: -1} == '"' ]]; then
v="${v:1:-1}"
v="${v//\\n/$'\n'}"
v="${v//\\\"/\"}"
v="${v//\\\\/\\}"
fi
echo "$v"
}
load_telegram_from_db() {
local cname="$1"
[[ -n "$cname" ]] || return 1
[[ -n "${BOT_TOKEN:-}" && -n "${CHAT_ID:-}" ]] && return 0
local out token chat
out="$(docker_cmd exec "$cname" python3 -c "
import asyncio
from server.config import settings as s
from server.infrastructure.database.session import AsyncSessionLocal
async def main():
async with AsyncSessionLocal() as db:
await s.load_settings_from_db(db)
t = (s.TELEGRAM_BOT_TOKEN or '').strip()
c = (s.TELEGRAM_CHAT_ID or '').strip()
if t and c:
print(t)
print(c)
asyncio.run(main())
" 2>/dev/null)" || true
[[ -n "$out" ]] || return 1
token="$(printf '%s\n' "$out" | sed -n '1p')"
chat="$(printf '%s\n' "$out" | sed -n '2p')"
[[ -n "$token" && -n "$chat" ]] || return 1
BOT_TOKEN="$token"
CHAT_ID="$chat"
}
load_telegram_config() {
local cname="$1" dotenv
BOT_TOKEN=""
CHAT_ID=""
for dotenv in \
"${DEPLOY_DIR}/.env" \
"$ENV_PROD" \
; do
[[ -f "$dotenv" ]] || continue
BOT_TOKEN="$(read_env_var NEXUS_TELEGRAM_BOT_TOKEN "$dotenv")"
CHAT_ID="$(read_env_var NEXUS_TELEGRAM_CHAT_ID "$dotenv")"
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
done
if [[ -n "$cname" ]]; then
local line
while IFS= read -r line; do
case "$line" in
NEXUS_TELEGRAM_BOT_TOKEN=*) BOT_TOKEN="${line#NEXUS_TELEGRAM_BOT_TOKEN=}" ;;
NEXUS_TELEGRAM_CHAT_ID=*) CHAT_ID="${line#NEXUS_TELEGRAM_CHAT_ID=}" ;;
esac
done < <(docker_cmd exec "$cname" grep -E '^NEXUS_TELEGRAM_' /app/.env 2>/dev/null || true)
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
load_telegram_from_db "$cname" || true
fi
BOT_TOKEN="$(normalize_env_value "$BOT_TOKEN")"
CHAT_ID="$(normalize_env_value "$CHAT_ID")"
}
send_telegram() {
local msg="${1}" resp http log="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
BOT_TOKEN="$(normalize_env_value "${BOT_TOKEN:-}")"
CHAT_ID="$(normalize_env_value "${CHAT_ID:-}")"
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] || return 1
resp="$(curl -s --max-time 15 -w '\n%{http_code}' \
"https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
--data-urlencode "text=${msg}" \
-d "parse_mode=HTML" 2>/dev/null)" || return 1
http="${resp##*$'\n'}"
if [[ "$http" != "200" ]]; then
echo "$(date -Is 2>/dev/null || date) telegram send failed http=${http}" >> "$log" 2>/dev/null || true
return 1
fi
}
bump_fail_count() {
local fail=0
init_layer3_state
if command -v flock >/dev/null 2>&1; then
(
flock -x 9
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
fail=$((fail + 1))
write_state_file "$FAIL_FILE" "$fail" || true
) 9>"${FAIL_FILE}.lock"
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
else
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
fail=$((fail + 1))
write_state_file "$FAIL_FILE" "$fail" || true
fi
echo "$fail"
}
reset_fail_count() {
write_state_file "$FAIL_FILE" "0" || true
}
restart_cooldown_active() {
local now last elapsed
[[ -f "$LAST_RESTART_FILE" ]] || return 1
now="$(date +%s)"
last="$(cat "$LAST_RESTART_FILE" 2>/dev/null || echo 0)"
elapsed=$((now - last))
[[ "$elapsed" -lt "$RESTART_COOLDOWN_SEC" ]]
}
mark_restart_attempt() {
write_state_file "$LAST_RESTART_FILE" "$(date +%s)" || true
}
restart_nexus() {
local cname="$1" via=""
if [[ -n "$cname" ]]; then
docker_cmd restart "$cname" >/dev/null 2>&1 && via="Docker 容器 ${cname}"
elif command -v supervisorctl >/dev/null 2>&1 && supervisorctl status "$NEXUS_SERVICE" >/dev/null 2>&1; then
supervisorctl restart "$NEXUS_SERVICE" >/dev/null 2>&1 && via="Supervisor ${NEXUS_SERVICE}"
else
return 1
fi
echo "$via"
}
telegram_health_fail_cooldown() {
local body
body="$(cat <<EOF
🔴 <b>【Layer 3 宿主机巡检】健康检查连续失败(冷却期内)</b>
宿主机:${HOST_LABEL}
检测地址:${HEALTH_URL}
连续失败:已达 ${MAX_FAIL} 次
Docker 容器:${NEXUS_CONTAINER:-未识别}
具体情况:/health 仍不可达,但距上次自动重启不足 ${RESTART_COOLDOWN_SEC} 秒,本次跳过重启以免频繁抖动
建议操作:查看 docker logs ${NEXUS_CONTAINER:-nexus} 与 ${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}
时间:${TS_LABEL}
EOF
)"
send_telegram "$body" || true
}
telegram_health_fail_restart() {
local body
body="$(cat <<EOF
🔴 <b>【Layer 3 宿主机巡检】Nexus 连续 ${MAX_FAIL} 次无响应</b>
宿主机:${HOST_LABEL}
检测地址:${HEALTH_URL}
连续失败:${FAIL} 次(阈值 ${MAX_FAIL} 次)
Docker 容器:${NEXUS_CONTAINER:-未识别}
具体情况:宿主机 cron 无法访问 API 健康端点,平台可能已宕机或端口异常
正在执行:自动重启 Nexus 服务…
时间:${TS_LABEL}
EOF
)"
send_telegram "$body" || true
}
telegram_health_recovered() {
local via="$1" body
body="$(cat <<EOF
🟢 <b>【Layer 3 宿主机巡检】Nexus 已自动恢复</b>
宿主机:${HOST_LABEL}
检测地址:${HEALTH_URL}
恢复方式:${via}
具体情况:自动重启后 /health 检测通过,服务已恢复在线
时间:${TS_LABEL}
EOF
)"
send_telegram "$body" || true
}
telegram_health_still_down() {
local via="$1" body
body="$(cat <<EOF
🔴 <b>【Layer 3 宿主机巡检】自动重启后仍未恢复</b>
宿主机:${HOST_LABEL}
检测地址:${HEALTH_URL}
已尝试:${via}
具体情况:重启完成后 /health 仍失败,可能存在数据库/Redis 故障或配置错误
建议操作:立即 SSH 登录排查 docker logs、MySQL、Redis,需人工介入
时间:${TS_LABEL}
EOF
)"
send_telegram "$body" || true
}
telegram_health_restart_failed() {
local body
body="$(cat <<EOF
🔴 <b>【Layer 3 宿主机巡检】无法执行自动重启</b>
宿主机:${HOST_LABEL}
检测地址:${HEALTH_URL}
具体情况:未找到可重启的 Docker 容器或 Supervisor 服务(${NEXUS_SERVICE}
建议操作:确认 nexus 容器名称与 docker ps 输出,手动执行 docker restart 或 nx update
时间:${TS_LABEL}
EOF
)"
send_telegram "$body" || true
}
NEXUS_CONTAINER="$(resolve_nexus_container)"
load_telegram_config "$NEXUS_CONTAINER"
init_layer3_state
HOST_LABEL="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo unknown)"
TS_LABEL="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || date)"
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
reset_fail_count
exit 0
fi
FAIL="$(bump_fail_count)"
if [[ "$FAIL" -lt "$MAX_FAIL" ]]; then
exit 0
fi
if restart_cooldown_active; then
telegram_health_fail_cooldown
exit 0
fi
telegram_health_fail_restart
mark_restart_attempt
if VIA="$(restart_nexus "$NEXUS_CONTAINER")"; then
sleep 5
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
telegram_health_recovered "$VIA"
reset_fail_count
else
telegram_health_still_down "$VIA"
fi
else
# Python is responding normally
echo "0" > "$FAIL_FILE"
telegram_health_restart_failed
fi
+276
View File
@@ -0,0 +1,276 @@
#!/usr/bin/env bash
# =============================================================================
# Nexus 6.0 — 重装服务器一键入口(1Panel + Docker + Nexus
#
# 适用:全新 VPS / 重装系统后,root 一条命令装完整栈。
#
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
#
# 4 核 16G + 自定义域名:
# curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g --domain api.example.com
#
# 已装 1Panel,只装 Nexus Docker:
# curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
#
# 流程:
# ① 可选:官方 1Panel 安装(含 Docker,交互式)
# ② 检测 / 安装 Docker Compose
# ③ install-nexus-fresh.sh → MySQL + Nexus 容器(Redis 自行安装)
#
# 反代:必须在 1Panel 面板 UI 配置,禁止脚本直接改 /opt/1panel/apps/
# 示例: deploy/1panel/openresty-nexus.conf.example
# =============================================================================
set -euo pipefail
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
PANEL_INSTALL_URL="${PANEL_INSTALL_URL:-https://resource.1panel.pro/v2/quick_start.sh}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
WITH_1PANEL=auto
SKIP_1PANEL=false
SKIP_DOCKER_INSTALL=false
EXTRA_ARGS=()
usage() {
cat <<EOF
重装服务器 — 1Panel + Docker + Nexus 一键安装
curl -fsSL "${NEXUS_RAW_BASE}/deploy/install-1panel-docker.sh" | bash
选项(传给 bash -s --:
--with-1panel 未装 1Panel 时先跑官方安装脚本(交互式)
--skip-1panel 跳过 1Panel,只装 Nexus Docker 栈
--skip-docker-install 不自动装 Docker(已在 1Panel 装好)
--profile 2c8g|4c16g|1c4g
--domain NAME
--root PATH
-h, --help
说明:
- 1Panel 官方安装为交互式(端口/用户/密码),装完记下面板地址
- Nexus 密钥按本机自动生成,备份在 /root/.nexus-install-secrets-*.env
- 网站反代请在 1Panel UI 操作,勿手工改 /opt/1panel/apps/
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--with-1panel) WITH_1PANEL=true; shift ;;
--skip-1panel) SKIP_1PANEL=true; WITH_1PANEL=false; shift ;;
--skip-docker-install) SKIP_DOCKER_INSTALL=true; shift ;;
--profile|--domain|--root)
EXTRA_ARGS+=("$1" "$2")
shift 2
;;
--skip-clone|--skip-1panel-check)
EXTRA_ARGS+=("$1")
shift
;;
*) EXTRA_ARGS+=("$1"); shift ;;
esac
done
banner() {
echo ""
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}${NC} Nexus — 1Panel + Docker + Nexus 一键安装 ${BLUE}${NC}"
echo -e "${BLUE}${NC} 域名: ${NEXUS_DOMAIN} 档位: ${NEXUS_PROFILE} ${BLUE}${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
require_root() {
if [[ "$(id -u)" -ne 0 ]]; then
error "请使用 root 执行(su - 后直接运行)"
exit 1
fi
}
load_deploy_env() {
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
done
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
}
has_1panel() {
command -v 1pctl >/dev/null 2>&1
}
has_docker_compose() {
docker compose version >/dev/null 2>&1
}
phase_1panel() {
if [[ "$SKIP_1PANEL" == true ]]; then
info "跳过 1Panel 安装(--skip-1panel"
return 0
fi
if has_1panel; then
info "已安装 1Panel"
if 1pctl user-info >/dev/null 2>&1; then
1pctl user-info 2>/dev/null | grep -E '面板地址|面板用户' || true
else
warn "1pctl user-info 失败,面板可能需修复;继续安装 Nexus…"
fi
return 0
fi
if [[ "$WITH_1PANEL" == auto ]]; then
echo ""
warn "未检测到 1Panel1pctl 不存在)"
echo " ① 先装 1Panel(推荐,含 Docker)再装 Nexus"
echo " ② 跳过 1Panel,仅装 Nexus Docker(需本机已有 Docker"
echo ""
read -r -p "是否现在安装 1Panel? [Y/n]: " ans
case "${ans:-Y}" in
n|N) WITH_1PANEL=false ;;
*) WITH_1PANEL=true ;;
esac
fi
if [[ "$WITH_1PANEL" != true ]]; then
warn "未安装 1Panel,将仅安装 Nexus Docker 栈"
return 0
fi
step "安装 1Panel(官方脚本,交互式)..."
info "下载: $PANEL_INSTALL_URL"
local tmp
tmp="$(mktemp /tmp/1panel-quick-start.XXXXXX.sh)"
if ! curl -fsSL "$PANEL_INSTALL_URL" -o "$tmp"; then
error "下载 1Panel 安装脚本失败"
exit 1
fi
chmod +x "$tmp"
echo ""
warn "接下来为 1Panel 官方安装向导,请按提示设置端口、安全入口、用户名、密码"
warn "安装目录建议默认 /opt"
echo ""
bash "$tmp"
rm -f "$tmp"
if ! has_1panel; then
error "1Panel 安装后仍未找到 1pctl,请检查安装日志"
exit 1
fi
info "1Panel 安装完成"
1pctl user-info 2>/dev/null || true
echo ""
warn "请在 1Panel → 应用商店 安装 OpenResty(用于 HTTPS 反代)"
read -r -p "OpenResty 已装好或稍后自行安装,按 Enter 继续 Nexus 安装..."
}
install_docker_engine() {
step "安装 Docker Engine + Compose 插件..."
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq ca-certificates curl >/dev/null
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker 2>/dev/null || true
elif command -v dnf >/dev/null 2>&1; then
dnf install -y -q docker docker-compose-plugin >/dev/null 2>&1 || \
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker 2>/dev/null || true
else
curl -fsSL https://get.docker.com | sh
fi
}
phase_docker() {
step "检测 Docker Compose..."
if has_docker_compose; then
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version | head -1)"
return 0
fi
if [[ "$SKIP_DOCKER_INSTALL" == true ]]; then
error "未检测到 docker compose,且已指定 --skip-docker-install"
error "请在 1Panel → 容器 安装 Docker,或去掉该参数"
exit 1
fi
warn "未检测到 docker compose"
if has_1panel; then
warn "建议在 1Panel → 容器 安装 Docker 后重新运行本脚本"
read -r -p "是否尝试用 get.docker.com 自动安装? [y/N]: " ans
[[ "${ans:-N}" == y || "${ans:-N}" == Y ]] || exit 1
else
info "自动安装 Dockerget.docker.com..."
fi
install_docker_engine
if ! has_docker_compose; then
error "Docker 安装后仍无 compose 插件,请手动安装 docker-compose-plugin"
exit 1
fi
info "Docker Compose 就绪"
}
download_fresh_installer() {
local name="install-nexus-fresh.sh" dest
dest="$(mktemp /tmp/install-nexus-fresh.XXXXXX.sh)"
local url base
for base in "${NEXUS_RAW_BASE}" "${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
url="${base}/deploy/${name}"
if curl -fsSL "$url" -o "$dest" 2>/dev/null && head -1 "$dest" | grep -q '^#!/'; then
chmod +x "$dest"
echo "$dest"
return 0
fi
done
return 1
}
phase_nexus() {
step "安装 Nexus Docker 栈(MySQL + NexusRedis 请自行安装)..."
local installer
if [[ -x "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" ]]; then
installer="${NEXUS_ROOT}/deploy/install-nexus-fresh.sh"
info "使用本地: $installer"
exec bash "$installer" --skip-1panel-check "${EXTRA_ARGS[@]}"
fi
if installer="$(download_fresh_installer)"; then
info "已下载 install-nexus-fresh.sh"
exec bash "$installer" --skip-1panel-check "${EXTRA_ARGS[@]}"
fi
error "无法获取 install-nexus-fresh.sh,请检查 Gitea 可达性"
exit 1
}
main() {
banner
require_root
load_deploy_env
phase_1panel
phase_docker
phase_nexus
}
# curl | bash 时 stdin 脚本无 BASH_SOURCE[0],不可配合 set -u 做入口判断
main "$@"
+310
View File
@@ -0,0 +1,310 @@
#!/usr/bin/env bash
# =============================================================================
# Nexus 6.0 — 全新安装脚本(1Panel + Docker Compose
#
# 适用:新机已装 1Panel / Docker,空库,走 /app/install.html 安装向导
# 风格参考 1Panel quick_start.sh:分阶段检查 + 结束输出访问地址
#
# 用法(公共仓库,root 下一条命令,参考 1Panel:
#
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
#
# 已 clone 到 /opt/nexus:
# cd /opt/nexus && bash deploy/install-nexus-fresh.sh --skip-clone
#
# bash deploy/install-nexus-fresh.sh --profile 4c16g
#
# 公共仓库匿名 clone,无需 Gitea 令牌。NEXUS 密钥由安装脚本按机自动生成。
# =============================================================================
set -euo pipefail
_install_self="$(readlink -f "${BASH_SOURCE[0]}")"
if ! head -1 "$_install_self" | grep -q '^#!/'; then
echo "[ERROR] 脚本文件无效。若用 curl 下载,请改用: bash deploy/download-install-fresh.sh" >&2
exit 1
fi
if grep -qiE '^(Not found|404)' "$_install_self" 2>/dev/null; then
echo "[ERROR] 下载到的是 Gitea 404 页面,不是安装脚本。" >&2
echo " 请: cd /opt/nexus && git pull && sudo bash deploy/install-nexus-fresh.sh --skip-clone" >&2
exit 1
fi
# ── 默认配置(与 SECRETS.md / nexus-1panel 一致)──
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
usage() {
cat <<EOF
Nexus 6.0 全新安装(1Panel + Docker
sudo bash install-nexus-fresh.sh [选项]
选项:
--domain NAME 公网域名(默认: ${NEXUS_DOMAIN}
--profile NAME 资源档位 1c4g|2c8g|4c16g(默认: 2c8g
--root PATH 安装目录(默认: /opt/nexus
--skip-clone 已在 /opt/nexus,不重新 clone
--skip-1panel-check 不检测 1Panel 是否已安装
-h, --help 显示帮助
安装后请用浏览器打开: https://域名/app/install.html
root 下全局命令: nexus-fresh | nexus-update | nx
EOF
}
SKIP_1PANEL_CHECK=false
SKIP_CLONE=false
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--domain) NEXUS_DOMAIN="$2"; shift 2 ;;
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
--root) NEXUS_ROOT="$2"; shift 2 ;;
--skip-clone) SKIP_CLONE=true; shift ;;
--skip-1panel-check) SKIP_1PANEL_CHECK=true; shift ;;
*) error "未知参数: $1"; usage; exit 1 ;;
esac
done
banner() {
echo ""
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}${NC} Nexus 6.0 — 全新安装(1Panel + Docker Compose ${BLUE}${NC}"
echo -e "${BLUE}${NC} 域名: ${NEXUS_DOMAIN} ${BLUE}${NC}"
echo -e "${BLUE}${NC} 档位: ${NEXUS_PROFILE} 目录: ${NEXUS_ROOT} ${BLUE}${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
load_credentials() {
step "加载凭据..."
local f
for f in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
if [[ -f "$f" ]]; then
# shellcheck disable=SC1090
set -a
source "$f"
set +a
info "已加载 $f"
fi
done
if [[ -f "${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh" ]]; then
# shellcheck disable=SC1090
set -a
source "${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh"
set +a
info "已加载 ${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh(可选)"
fi
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
info "全新安装:NEXUS 密钥将在 docker/.env.prod 中按本机自动生成"
}
install_root_commands() {
[[ -d "${NEXUS_ROOT}/deploy" ]] || return 0
NEXUS_ROOT="$NEXUS_ROOT" bash "${NEXUS_ROOT}/deploy/install-nx-cli.sh" || true
info "全局命令: nx · nexus-update · nexus-fresh · nexus-install · nexus-1panel"
}
phase_preflight() {
step "阶段 1/6 — 环境检查"
if [[ "$(id -u)" -ne 0 ]]; then
error "请切换到 root 再执行(不要用 sudo 包一层):"
error " su -"
error " bash $0 ..."
exit 1
fi
info "运行用户: root(无需 sudo"
if [[ ! -f /etc/os-release ]]; then
error "无法识别操作系统"
exit 1
fi
# shellcheck disable=SC1091
source /etc/os-release
info "系统: ${NAME:-Linux} ${VERSION_ID:-}"
case "${ID:-}" in
ubuntu|debian) info "受支持的分支: Debian/Ubuntu ✓" ;;
centos|rhel|rocky|almalinux) info "受支持的分支: RHEL 系 ✓" ;;
*) warn "未在官方列表中测试,可能仍可运行" ;;
esac
local mem_kb
mem_kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)"
if [[ "$mem_kb" -lt 3500000 ]]; then
warn "内存 < 4GB,建议档位 1c4g: --profile 1c4g"
else
info "内存: $((mem_kb / 1024 / 1024)) GiB 量级"
fi
}
phase_1panel() {
step "阶段 2/6 — 1Panel / Docker"
if [[ "$SKIP_1PANEL_CHECK" != true ]]; then
if command -v 1pctl >/dev/null 2>&1; then
info "检测到 1Panel (1pctl)"
if 1pctl version >/dev/null 2>&1; then
info "1Panel: $(1pctl version 2>/dev/null | head -1 || true)"
fi
else
warn "未检测到 1pctl — 若尚未安装 1Panel,可先执行:"
warn " curl -sSL https://resource.1panel.pro/v2/quick_start.sh -o quick_start.sh"
warn " bash quick_start.sh"
read -r -p "继续安装 Nexus? [y/N]: " cont
[[ "$cont" == "y" || "$cont" == "Y" ]] || exit 0
fi
fi
if ! docker compose version >/dev/null 2>&1; then
error "未检测到 docker compose"
error "请在 1Panel → 容器 中安装 Docker,或: apt install -y docker.io docker-compose-v2"
exit 1
fi
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version | head -1)"
}
phase_packages() {
step "阶段 3/6 — 依赖包"
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq git curl openssl ca-certificates iproute2 >/dev/null 2>&1 || true
elif command -v dnf >/dev/null 2>&1; then
dnf install -y -q git curl openssl ca-certificates iproute >/dev/null 2>&1 || true
fi
info "git / curl 就绪"
}
git_clone_url() {
if [[ -z "${NEXUS_GITEA_TOKEN:-}" ]]; then
echo "${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
else
echo "${NEXUS_GITEA_SCHEME}://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
fi
}
phase_clone() {
step "阶段 4/6 — 获取 Nexus 代码"
if [[ "$SKIP_CLONE" == true ]]; then
if [[ ! -d "${NEXUS_ROOT}/.git" ]]; then
error "--skip-clone 但 ${NEXUS_ROOT} 不是 git 仓库"
exit 1
fi
load_credentials
info "跳过 clone,使用现有 ${NEXUS_ROOT}"
info "版本: $(git -C "$NEXUS_ROOT" log -1 --oneline 2>/dev/null || echo '?')"
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
return 0
fi
load_credentials
local url
url="$(git_clone_url)"
if [[ -z "${NEXUS_GITEA_TOKEN:-}" ]]; then
info "公共仓库:匿名 git clone(无需令牌)"
fi
mkdir -p "$(dirname "$NEXUS_ROOT")"
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
info "仓库已存在,同步 ${NEXUS_GIT_BRANCH}..."
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --prune
git -C "$NEXUS_ROOT" reset --hard "origin/${NEXUS_GIT_BRANCH}"
else
if [[ -d "$NEXUS_ROOT" ]] && [[ -n "$(ls -A "$NEXUS_ROOT" 2>/dev/null)" ]]; then
error "目录非空且不是 git 仓库: $NEXUS_ROOT"
exit 1
fi
rm -rf "$NEXUS_ROOT"
info "克隆 → $NEXUS_ROOT"
git clone "$url" "$NEXUS_ROOT"
git -C "$NEXUS_ROOT" checkout "$NEXUS_GIT_BRANCH" 2>/dev/null || true
fi
load_credentials
info "版本: $(git -C "$NEXUS_ROOT" log -1 --oneline)"
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
}
phase_install_stack() {
step "阶段 5/6 — Docker 构建与启动(全新库,约 10~20 分钟)"
export NEXUS_ROOT NEXUS_DOMAIN NEXUS_PROFILE
bash "${NEXUS_ROOT}/deploy/nexus-1panel.sh" install \
--fresh \
--skip-clone \
--profile "$NEXUS_PROFILE" \
--domain "$NEXUS_DOMAIN"
}
phase_post() {
step "阶段 6/6 — 收尾"
install_root_commands
local port health
port="8600"
health="$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || echo "pending")"
echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}${NC} ${GREEN}Nexus 全新安装完成${NC} ${GREEN}${NC}"
echo -e "${GREEN}╠══════════════════════════════════════════════════════════════╣${NC}"
echo -e "${GREEN}${NC} 本地健康: http://127.0.0.1:${port}/health → ${health} ${GREEN}${NC}"
echo -e "${GREEN}${NC} ${GREEN}${NC}"
echo -e "${GREEN}${NC} ${YELLOW}① 浏览器完成安装向导(必做)${NC} ${GREEN}${NC}"
echo -e "${GREEN}${NC} https://${NEXUS_DOMAIN}/app/install.html ${GREEN}${NC}"
echo -e "${GREEN}${NC} ${GREEN}${NC}"
echo -e "${GREEN}${NC} ${YELLOW}② 1Panel 创建网站并反代${NC} ${GREEN}${NC}"
echo -e "${GREEN}${NC} 目标: http://127.0.0.1:${port} ${GREEN}${NC}"
echo -e "${GREEN}${NC} 示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example ${GREEN}${NC}"
echo -e "${GREEN}${NC} ${GREEN}${NC}"
echo -e "${GREEN}${NC} ${YELLOW}③ 日常运维${NC} ${GREEN}${NC}"
echo -e "${GREEN}${NC} nx # 统一菜单(一键更新 / 重启 / 日志) ${GREEN}${NC}"
echo -e "${GREEN}${NC} nexus-update # 等同 deploy/update.sh ${GREEN}${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
warn_if_already_installed() {
local env_prod="${NEXUS_ROOT}/docker/.env.prod" cname
[[ -f "$env_prod" ]] || return 0
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
if [[ -n "$cname" ]] && docker exec "$cname" test -f /var/lib/nexus/.install_locked 2>/dev/null; then
warn "检测到已安装并锁定的 Nexus(将清除 nexus-state 卷内配置并重建)"
read -r -p "确认继续全新安装? [y/N]: " ok
[[ "$ok" == "y" || "$ok" == "Y" ]] || exit 0
fi
}
main() {
banner
if [[ "$SKIP_CLONE" == true && -d "${NEXUS_ROOT}/deploy" ]]; then
install_root_commands
fi
warn_if_already_installed
phase_preflight
phase_1panel
phase_packages
phase_clone
phase_install_stack
phase_post
}
main "$@"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# 注册 Nexus 全局 CLI/usr/local/bin)— install / upgrade / nx 均会调用
#
# NEXUS_ROOT=/opt/nexus bash deploy/install-nx-cli.sh
#
set -euo pipefail
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
BIN_DIR="${NEXUS_BIN_DIR:-/usr/local/bin}"
DEPLOY="${NEXUS_ROOT}/deploy"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
if [[ ! -d "$DEPLOY" ]]; then
error "未找到 $DEPLOY"
exit 1
fi
if [[ "$(id -u)" -ne 0 ]]; then
warn "非 root:无法写入 $BIN_DIR,请使用 root 执行或: sudo bash $DEPLOY/install-nx-cli.sh"
exit 0
fi
chmod +x \
"$DEPLOY/nx" \
"$DEPLOY/update.sh" \
"$DEPLOY/nexus-1panel.sh" \
"$DEPLOY/install-nexus-fresh.sh" \
"$DEPLOY/quick-install.sh" \
"$DEPLOY/install-1panel-docker.sh" \
"$DEPLOY/upgrade-1panel-docker.sh" \
"$DEPLOY/sync-install-wizard-to-container.sh" \
"$DEPLOY/uninstall-mysql-compose.sh" \
"$DEPLOY/uninstall-redis-compose.sh" \
"$DEPLOY/install-nx-cli.sh" \
2>/dev/null || true
mkdir -p "$BIN_DIR"
link_cli() {
local src="$1" name="$2"
if [[ ! -f "$src" ]]; then
warn "跳过 $name(缺少 $src"
return 0
fi
ln -sf "$src" "${BIN_DIR}/${name}"
}
link_cli "$DEPLOY/nx" nx
link_cli "$DEPLOY/update.sh" nexus-update
link_cli "$DEPLOY/install-nexus-fresh.sh" nexus-fresh
link_cli "$DEPLOY/quick-install.sh" nexus-install
link_cli "$DEPLOY/nexus-1panel.sh" nexus-1panel
link_cli "$DEPLOY/install-1panel-docker.sh" nexus-1panel-docker
if command -v nx >/dev/null 2>&1; then
info "全局命令已就绪: nx → $(readlink -f "$(command -v nx)" 2>/dev/null || command -v nx)"
else
error "nx 未出现在 PATH 中,请确认 $BIN_DIR 在 PATH 内"
exit 1
fi
+526
View File
@@ -0,0 +1,526 @@
#!/bin/bash
# ================================================================
# Nexus 6.0 — One-Click Install Script
#
# Usage:
# curl -fsSL https://YOUR_DOMAIN/deploy/install.sh | sudo bash -s -- --domain nexus.example.com
# sudo bash install.sh --domain nexus.example.com
#
# Supports: Ubuntu 20.04+, Debian 11+, CentOS 7/8, Rocky 8/9, AlmaLinux 8/9
# Auto-detects BT Panel (宝塔面板) and adapts paths accordingly.
# ================================================================
set -euo pipefail
# ── Colors ──
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; }
# ── Helper: Idempotent config write ──
write_config() {
local target="$1" content="$2"
if [ -f "$target" ]; then
local existing
existing=$(cat "$target" 2>/dev/null || true)
if [ "$existing" = "$content" ]; then
info " [skip] $target (unchanged)"
return 0
fi
info " [update] $target (content changed)"
else
info " [create] $target"
fi
echo "$content" > "$target"
}
# ── Defaults ──
DOMAIN=""
DEPLOY_DIR=""
PORT=8600
SKIP_NGINX=false
SKIP_SUPERVISOR=false
BT_MODE=false
NEXUS_REPO=""
# ── Parse Arguments ──
while [[ $# -gt 0 ]]; do
case $1 in
--domain) DOMAIN="$2"; shift 2 ;;
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
--port) PORT="$2"; shift 2 ;;
--skip-nginx) SKIP_NGINX=true; shift ;;
--skip-supervisor) SKIP_SUPERVISOR=true; shift ;;
--repo) NEXUS_REPO="$2"; shift 2 ;;
-h|--help)
echo "Usage: sudo bash install.sh --domain DOMAIN [options]"
echo ""
echo "Options:"
echo " --domain DOMAIN Required. Domain or IP for Nginx server_name"
echo " --deploy-dir PATH Installation directory (auto-detected)"
echo " --port PORT uvicorn port (default: 8600)"
echo " --skip-nginx Skip Nginx configuration"
echo " --skip-supervisor Skip Supervisor configuration"
echo " --repo URL Git repository URL (optional, for cloning)"
exit 0 ;;
*) shift ;;
esac
done
# ── Banner ──
echo ""
echo "=========================================="
echo " Nexus 6.0 Install Script"
echo "=========================================="
echo ""
# ================================================================
# Phase 1: Pre-flight Checks
# ================================================================
info "Phase 1/7: Pre-flight checks..."
# 1.1 Must run as root
if [ "$(id -u)" -ne 0 ]; then
error "This script must be run as root. Use: sudo bash install.sh"
exit 1
fi
# 1.2 Domain is required
if [ -z "$DOMAIN" ]; then
error "--domain is required. Usage: sudo bash install.sh --domain nexus.example.com"
exit 1
fi
# 1.3 OS detection
if [ ! -f /etc/os-release ]; then
error "Cannot detect OS. /etc/os-release not found."
exit 1
fi
OS_ID=$(grep '^ID=' /etc/os-release | head -1 | cut -d'=' -f2 | tr -d '"' | cut -d'-' -f1)
OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release | head -1 | cut -d'=' -f2 | tr -d '"')
case "$OS_ID" in
ubuntu|debian) PKG_MANAGER="apt" ;;
centos|rocky|almalinux|rhel) PKG_MANAGER="yum" ;;
*)
warn "Unsupported OS: $OS_ID. Script may still work but is untested."
PKG_MANAGER="apt"
;;
esac
info " OS: $OS_ID $OS_VERSION (pkg: $PKG_MANAGER)"
# 1.4 BT Panel detection
if [ -d "/www/server/panel" ] && [ -f "/www/server/panel/class/common.py" ]; then
BT_MODE=true
info " BT Panel: detected"
else
info " BT Panel: not detected (standard mode)"
fi
# 1.5 Set deploy directory
if [ -z "$DEPLOY_DIR" ]; then
if [ "$BT_MODE" = true ]; then
DEPLOY_DIR="/www/wwwroot/$DOMAIN"
else
DEPLOY_DIR="/opt/nexus"
fi
fi
VENV_DIR="$DEPLOY_DIR/venv"
# Set log paths early (used in multiple phases)
if [ "$BT_MODE" = true ]; then
LOG_DIR="/www/wwwlogs"
else
LOG_DIR="/var/log/nexus"
fi
info " Deploy dir: $DEPLOY_DIR"
info " Domain: $DOMAIN"
info " Port: $PORT"
# ================================================================
# Phase 2: System Dependencies
# ================================================================
info "Phase 2/7: Installing system dependencies..."
if [ "$PKG_MANAGER" = "apt" ]; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq \
python3 python3-venv python3-pip python3-dev \
build-essential libssl-dev libffi-dev default-libmysqlclient-dev \
git curl >/dev/null 2>&1 || warn "Some apt packages failed to install (may already exist)"
elif [ "$PKG_MANAGER" = "yum" ]; then
if command -v dnf >/dev/null 2>&1; then
dnf install -y -q \
python3 python3-devel python3-pip \
gcc openssl-devel libffi-devel mysql-devel \
git curl >/dev/null 2>&1 || warn "Some dnf packages failed to install"
else
yum install -y -q \
python3 python3-devel python3-pip \
gcc openssl-devel libffi-devel mysql-devel \
git curl >/dev/null 2>&1 || warn "Some yum packages failed to install"
fi
fi
# ================================================================
# Phase 3: Python 3.10+ Resolution
# ================================================================
info "Phase 3/7: Resolving Python 3.10+..."
PYTHON=""
CANDIDATES="python3.12 python3.11 python3.10 /usr/bin/python3.12 /usr/bin/python3.11 /usr/bin/python3.10 python3"
# Add BT Panel Python paths
if [ "$BT_MODE" = true ]; then
for p in /www/server/python-manager/versions/*/bin/python3.*; do
[ -x "$p" ] && CANDIDATES="$CANDIDATES $p"
done
fi
for candidate in $CANDIDATES; do
if command -v "$candidate" >/dev/null 2>&1 || [ -x "$candidate" ]; then
ver=$("$candidate" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)
if [ -n "$ver" ]; then
major=$(echo "$ver" | cut -d'.' -f1)
minor=$(echo "$ver" | cut -d'.' -f2)
if [ "$major" -eq 3 ] && [ "${minor:-0}" -ge 10 ] 2>/dev/null; then
PYTHON="$candidate"
break
fi
fi
fi
done
# Fallback: install Python 3.12 on Ubuntu via deadsnakes PPA
if [ -z "$PYTHON" ] && [ "$OS_ID" = "ubuntu" ]; then
warn " Python 3.10+ not found. Adding deadsnakes PPA..."
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -qq software-properties-common >/dev/null 2>&1
add-apt-repository -y ppa:deadsnakes/ppa >/dev/null 2>&1
apt-get update -qq
apt-get install -y -qq python3.12 python3.12-venv python3.12-dev >/dev/null 2>&1
if command -v python3.12 >/dev/null 2>&1; then
PYTHON="python3.12"
fi
fi
if [ -z "$PYTHON" ]; then
error "Python 3.10+ is required but not found."
error "Install Python 3.10+ manually and re-run."
exit 1
fi
PYTHON_VERSION=$("$PYTHON" --version 2>&1)
info " Using: $PYTHON ($PYTHON_VERSION)"
# ================================================================
# Phase 4: Application Deployment
# ================================================================
info "Phase 4/7: Deploying application..."
# 4.1 Create deploy directory
mkdir -p "$DEPLOY_DIR"
# 4.2 Download / Clone Nexus
if [ -d "$DEPLOY_DIR/server" ] && [ -d "$DEPLOY_DIR/web" ]; then
info " Application files already exist in $DEPLOY_DIR (skipping download)"
if [ -d "$DEPLOY_DIR/.git" ]; then
info " Git repo detected, pulling latest..."
(cd "$DEPLOY_DIR" && git pull --ff-only 2>/dev/null || warn " git pull failed (may have local changes)")
fi
elif [ -n "$NEXUS_REPO" ]; then
info " Cloning from $NEXUS_REPO..."
git clone "$NEXUS_REPO" "$DEPLOY_DIR" 2>/dev/null || {
error "git clone failed. Check the repository URL."
exit 1
}
else
error "No application files found in $DEPLOY_DIR and no --repo URL provided."
error "Either:"
error " 1. Copy Nexus files to $DEPLOY_DIR manually and re-run"
error " 2. Use --repo https://your-gitea-url/Nexus.git"
exit 1
fi
# 4.3 Create Python venv
if [ -d "$VENV_DIR/bin" ] && "$VENV_DIR/bin/python" -c 'import sys; v=sys.version_info; assert v.major==3 and v.minor>=10' 2>/dev/null; then
info " venv already exists with Python 3.10+ (skipping)"
else
info " Creating Python venv..."
rm -rf "$VENV_DIR"
"$PYTHON" -m venv "$VENV_DIR"
fi
# 4.4 Install pip dependencies
info " Installing Python dependencies (this may take a few minutes)..."
"$VENV_DIR/bin/pip" install -q --upgrade pip
"$VENV_DIR/bin/pip" install -q -r "$DEPLOY_DIR/requirements.txt" 2>/dev/null || {
warn " pip install had some warnings, but continuing..."
}
# 4.5 Create log directory
mkdir -p "$LOG_DIR"
# 4.6 Create backup directory
mkdir -p "$DEPLOY_DIR/backups"
# ================================================================
# Phase 5: Supervisor Configuration
# ================================================================
if [ "$SKIP_SUPERVISOR" = true ]; then
warn "Phase 5/7: Skipping Supervisor (per --skip-supervisor)"
else
info "Phase 5/7: Configuring Supervisor..."
# Calculate worker count
WORKERS=$(nproc 2>/dev/null || echo 2)
[ "$WORKERS" -gt 4 ] && WORKERS=4
[ "$WORKERS" -lt 1 ] && WORKERS=1
# Set Supervisor config path
if [ "$BT_MODE" = true ]; then
SUPERVISOR_CONF_DIR="/www/server/panel/plugin/supervisor/profile"
CONF_FILE="$SUPERVISOR_CONF_DIR/nexus.ini"
else
SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d"
CONF_FILE="$SUPERVISOR_CONF_DIR/nexus.conf"
fi
# Generate Supervisor config
SUPERVISOR_CONF="[program:nexus]
command=$VENV_DIR/bin/uvicorn server.main:app --host 0.0.0.0 --port $PORT --workers $WORKERS --loop uvloop --http httptools --log-level info
directory=$DEPLOY_DIR
user=root
autostart=true
autorestart=true
startretries=10
startsecs=3
stopwaitsecs=10
stopsignal=INT
environment=PATH=\"$VENV_DIR/bin:%(ENV_PATH)s\",HOME=\"/root\",NEXUS_WORKERS=\"$WORKERS\"
stderr_logfile=$LOG_DIR/nexus_error.log
stdout_logfile=$LOG_DIR/nexus_access.log
stderr_logfile_maxbytes=50MB
stdout_logfile_maxbytes=50MB
stderr_logfile_backups=5
stdout_logfile_backups=5"
# Write config (idempotent)
mkdir -p "$SUPERVISOR_CONF_DIR"
write_config "$CONF_FILE" "$SUPERVISOR_CONF"
# Ensure Supervisor is running
if ! supervisorctl status >/dev/null 2>&1; then
if [ "$BT_MODE" = true ]; then
if [ -f /etc/init.d/supervisor ]; then
/etc/init.d/supervisor start >/dev/null 2>&1 || true
fi
else
systemctl start supervisor >/dev/null 2>&1 || true
fi
sleep 2
fi
# Reload Supervisor config
supervisorctl reread >/dev/null 2>&1 || true
supervisorctl update >/dev/null 2>&1 || true
info " Supervisor config written to $CONF_FILE (workers=$WORKERS)"
fi
# ================================================================
# Phase 6: Nginx Configuration
# ================================================================
if [ "$SKIP_NGINX" = true ]; then
warn "Phase 6/7: Skipping Nginx (per --skip-nginx)"
else
info "Phase 6/7: Configuring Nginx..."
if [ "$BT_MODE" = true ]; then
NGINX_CONF_FILE="/www/server/panel/vhost/nginx/$DOMAIN.conf"
NGINX_RELOAD_CMD="/www/server/nginx/sbin/nginx -s reload"
NGINX_TEST_CMD="/www/server/nginx/sbin/nginx -t"
NGINX_ACCESS_LOG="/www/wwwlogs/$DOMAIN.log"
NGINX_ERROR_LOG="/www/wwwlogs/$DOMAIN.error.log"
else
NGINX_CONF_FILE="/etc/nginx/sites-available/nexus"
NGINX_RELOAD_CMD="systemctl reload nginx"
NGINX_TEST_CMD="nginx -t"
NGINX_ACCESS_LOG="/var/log/nginx/nexus_access.log"
NGINX_ERROR_LOG="/var/log/nginx/nexus_error.log"
fi
NGINX_CONF="# Nexus 6.0 — Nginx Reverse Proxy
# Auto-generated by install.sh
upstream nexus_api {
server 127.0.0.1:$PORT;
keepalive 32;
}
server {
listen 80;
server_name $DOMAIN;
# Reverse proxy to Nexus (uvicorn)
location / {
proxy_pass http://nexus_api;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
# WebSocket support (terminal, alerts)
location /ws/ {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \"upgrade\";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 3600s;
}
# Static assets caching
location /app/vendor/ {
proxy_pass http://nexus_api;
expires 7d;
add_header Cache-Control \"public, immutable\";
}
# Block sensitive files
location ~ ^/(\\.env|\\.git|\\.svn) {
return 404;
}
# Block config.json access
location ~ /data/config\\.json$ {
return 404;
}
# Upload limit
client_max_body_size 500m;
# Well-known for SSL cert validation
location ~ \\.well-known {
allow all;
}
access_log $NGINX_ACCESS_LOG;
error_log $NGINX_ERROR_LOG;
}"
# Write Nginx config
if [ "$BT_MODE" = true ]; then
write_config "$NGINX_CONF_FILE" "$NGINX_CONF"
else
mkdir -p /etc/nginx/sites-available
write_config "$NGINX_CONF_FILE" "$NGINX_CONF"
if [ ! -L /etc/nginx/sites-enabled/nexus ]; then
ln -sf "$NGINX_CONF_FILE" /etc/nginx/sites-enabled/nexus
fi
fi
# Test and reload Nginx
if $NGINX_TEST_CMD >/dev/null 2>&1; then
$NGINX_RELOAD_CMD >/dev/null 2>&1 || warn " Nginx reload failed"
info " Nginx config written to $NGINX_CONF_FILE"
else
warn " Nginx config test failed. Please check $NGINX_CONF_FILE manually"
fi
fi
# ================================================================
# Phase 7: Start Application + Verify
# ================================================================
info "Phase 7/7: Starting application..."
# Start via Supervisor
if [ "$SKIP_SUPERVISOR" = false ]; then
supervisorctl start nexus >/dev/null 2>&1 || supervisorctl restart nexus >/dev/null 2>&1 || true
fi
# Wait for health check
HEALTH_OK=false
for i in $(seq 1 15); do
if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
HEALTH_OK=true
break
fi
sleep 2
done
# ── Print Results ──
echo ""
echo "=========================================="
if [ "$HEALTH_OK" = true ]; then
echo -e " ${GREEN}Nexus installed successfully!${NC}"
else
echo -e " ${YELLOW}Nexus process started but health check pending.${NC}"
echo -e " ${YELLOW}It may need a few more seconds to start up.${NC}"
fi
echo "=========================================="
echo ""
echo " Deploy: $DEPLOY_DIR"
echo " Domain: $DOMAIN"
echo " Port: $PORT"
echo " Workers: ${WORKERS:-N/A}"
echo " BT Panel: $BT_MODE"
echo ""
if [ "$HEALTH_OK" = true ]; then
echo -e " ${GREEN}Next step:${NC} Open your browser and complete the web install wizard:"
echo ""
echo " http://$DOMAIN/app/install.html"
echo ""
echo " The wizard will guide you through:"
echo " Step 1: Welcome & prerequisites"
echo " Step 2: Environment check"
echo " Step 3: MySQL + Redis configuration"
echo " Step 4: Admin account + brand"
echo " Step 5: Complete"
else
echo " Troubleshooting:"
echo " supervisorctl status nexus"
echo " tail -f $LOG_DIR/nexus_error.log"
echo " curl http://127.0.0.1:$PORT/health"
fi
echo ""
echo " Post-install:"
echo " 1. Configure SSL (HTTPS)"
if [ "$BT_MODE" = true ]; then
echo " BT Panel -> Website -> $DOMAIN -> SSL"
else
echo " certbot --nginx -d $DOMAIN"
fi
echo " 2. Set up health monitor crontab:"
echo " * * * * * $DEPLOY_DIR/deploy/health_monitor.sh"
echo " 3. Set up database backup crontab:"
echo " 0 3 * * * $DEPLOY_DIR/deploy/db_backup.sh"
echo ""
echo " Management:"
echo " supervisorctl status nexus"
echo " supervisorctl restart nexus"
echo " supervisorctl stop nexus"
echo ""
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Nexus — 安装/更新 host crontabhealth_monitor + db_backup
#
# sudo bash deploy/install_ops_cron.sh
# NEXUS_ROOT=/opt/nexus sudo bash deploy/install_ops_cron.sh
set -euo pipefail
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
DEPLOY_DIR="${NEXUS_ROOT}/deploy"
HEALTH_MARKER="nexus-health-monitor"
BACKUP_MARKER="nexus-mysql-backup"
HEALTH_LOG="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
BACKUP_LOG="${NEXUS_BACKUP_LOG:-/var/log/nexus_backup.log}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
if [[ "$(id -u)" -ne 0 ]]; then
error "请使用 root 运行: sudo bash $0"
exit 1
fi
ensure_crontab() {
if command -v crontab >/dev/null 2>&1; then
return 0
fi
warn "未检测到 crontab,尝试安装 cron 包…"
if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq cron
systemctl enable --now cron 2>/dev/null || systemctl enable --now crond 2>/dev/null || true
elif command -v yum >/dev/null 2>&1; then
yum install -y cronie
systemctl enable --now crond 2>/dev/null || true
fi
if ! command -v crontab >/dev/null 2>&1; then
error "无法安装 crontab(请手动: apt install cron"
exit 1
fi
info "cron 已安装"
}
ensure_crontab
for f in \
"${DEPLOY_DIR}/health_monitor.sh" \
"${DEPLOY_DIR}/db_backup.sh" \
"${DEPLOY_DIR}/mysql_dump_to_file.sh" \
; do
if [[ ! -f "$f" ]]; then
error "缺少脚本: $f"
exit 1
fi
chmod +x "$f" 2>/dev/null || true
done
touch "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
chmod 644 "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
HEALTH_LINE="* * * * * NEXUS_DEPLOY_DIR=${NEXUS_ROOT} bash ${DEPLOY_DIR}/health_monitor.sh >> ${HEALTH_LOG} 2>&1 # ${HEALTH_MARKER}"
BACKUP_LINE="0 3 * * * NEXUS_ROOT=${NEXUS_ROOT} bash ${DEPLOY_DIR}/db_backup.sh >> ${BACKUP_LOG} 2>&1 # ${BACKUP_MARKER}"
existing="$(crontab -l 2>/dev/null || true)"
filtered="$(printf '%s\n' "$existing" | grep -v "$HEALTH_MARKER" | grep -v "$BACKUP_MARKER" | grep -v 'deploy/health_monitor.sh' | grep -v 'deploy/db_backup.sh' || true)"
{
printf '%s\n' "$filtered" | sed '/^[[:space:]]*$/d'
echo "$HEALTH_LINE"
echo "$BACKUP_LINE"
} | crontab -
info "已写入 crontab:"
crontab -l | grep -E "$HEALTH_MARKER|$BACKUP_MARKER" || true
info "健康巡检日志: $HEALTH_LOG"
info "MySQL 备份日志: $BACKUP_LOG"
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
# Nexus — MySQL dumpDocker 1Panel / 宿主机 mysqldump 通用)
#
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql [--root /opt/nexus]
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql --db-url 'mysql+aiomysql://...'
#
# 策略(按序):
# 1. DB host 为运行中容器名 → docker exec 该容器内 mysqldump(连 127.0.0.1
# 2. 宿主机有 mysqldump 且 host 可解析 → 本机 mysqldump
# 3. 1panel-network 存在 → docker run --rm mysql:8 客户端
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
OUTPUT=""
DB_URL=""
usage() {
cat <<'EOF'
用法: mysql_dump_to_file.sh --output FILE.sql [--root /opt/nexus] [--db-url URL]
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--output|-o) OUTPUT="$2"; shift 2 ;;
--root) NEXUS_ROOT="$2"; shift 2 ;;
--db-url) DB_URL="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "未知参数: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "$OUTPUT" ]]; then
echo "缺少 --output" >&2
usage
exit 1
fi
resolve_db_url() {
if [[ -n "$DB_URL" ]]; then
echo "$DB_URL"
return 0
fi
local cname url
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
if [[ -n "$cname" ]]; then
url="$(docker exec "$cname" grep -E '^NEXUS_DATABASE_URL=' /app/.env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
[[ -n "$url" ]] && echo "$url" && return 0
fi
for f in \
"${NEXUS_ROOT}/docker/.env.prod" \
"${NEXUS_ROOT}/.env" \
; do
[[ -f "$f" ]] || continue
url="$(grep -E '^NEXUS_DATABASE_URL=' "$f" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
[[ -n "$url" ]] && echo "$url" && return 0
done
return 1
}
DB_URL="$(resolve_db_url || true)"
if [[ -z "$DB_URL" ]]; then
echo "未找到 NEXUS_DATABASE_URL" >&2
exit 1
fi
python3 - "$DB_URL" "$OUTPUT" <<'PY'
import shutil
import subprocess
import sys
import urllib.parse
def docker_names() -> set[str]:
r = subprocess.run(
["docker", "ps", "--format", "{{.Names}}"],
capture_output=True,
text=True,
check=False,
)
return {ln.strip() for ln in (r.stdout or "").splitlines() if ln.strip()}
def network_exists(name: str) -> bool:
return (
subprocess.run(
["docker", "network", "inspect", name],
capture_output=True,
check=False,
).returncode
== 0
)
def write_dump(cmd: list[str], env: dict[str, str], out: str) -> int:
r = subprocess.run(cmd, env=env, capture_output=True, text=True)
if r.returncode != 0:
sys.stderr.write(r.stderr or r.stdout or "mysqldump failed\n")
return r.returncode
with open(out, "w", encoding="utf-8") as fh:
fh.write(r.stdout)
return 0
def dump_flags(db: str) -> list[str]:
return [
"--single-transaction",
"--routines",
"--triggers",
"--set-gtid-purged=OFF",
db,
]
url = sys.argv[1].replace("mysql+aiomysql://", "mysql://", 1)
url = url.replace("mysql+asyncmy://", "mysql://", 1)
out = sys.argv[2]
p = urllib.parse.urlparse(url)
user = urllib.parse.unquote(p.username or "")
password = urllib.parse.unquote(p.password or "")
host = p.hostname or "127.0.0.1"
if host == "host.docker.internal":
host = "127.0.0.1"
port = str(p.port or 3306)
db = (p.path or "/nexus").lstrip("/").split("?")[0]
env = {**__import__("os").environ, "MYSQL_PWD": password}
names = docker_names()
# 1) MySQL 跑在 Docker 容器内(1Panel 常见:URL host = 容器名)
if host in names:
cmd = [
"docker",
"exec",
"-e",
f"MYSQL_PWD={password}",
host,
"mysqldump",
"-h",
"127.0.0.1",
"-P",
port,
"-u",
user,
*dump_flags(db),
]
rc = write_dump(cmd, env, out)
if rc == 0:
sys.exit(0)
sys.stderr.write(f"docker exec {host} mysqldump 失败,尝试其它方式…\n")
# 2) 宿主机 mysqldump
if shutil.which("mysqldump"):
cmd = [
"mysqldump",
"-h",
host,
"-P",
port,
"-u",
user,
*dump_flags(db),
]
rc = write_dump(cmd, env, out)
if rc == 0:
sys.exit(0)
# 3) 临时 mysql 客户端容器(1panel-network
for net in ("1panel-network", "bridge"):
if not network_exists(net):
continue
cmd = [
"docker",
"run",
"--rm",
"--network",
net,
"-e",
f"MYSQL_PWD={password}",
"mysql:8.0",
"mysqldump",
"-h",
host,
"-P",
port,
"-u",
user,
*dump_flags(db),
]
rc = write_dump(cmd, env, out)
if rc == 0:
sys.exit(0)
sys.stderr.write("所有 mysqldump 策略均失败(无宿主机客户端且 Docker 回退不可用)\n")
sys.exit(1)
PY
+22
View File
@@ -0,0 +1,22 @@
# 复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh
# 用于 git pushscripts/git-push.sh);clone/pull 仍可匿名
# NEXUS_GITEA_HOST='axs.kuma1xn.vip' # HTTPS Gitea
# NEXUS_GITEA_SCHEME='https'
# NEXUS_GITEA_REPO='admin/Nexus.git'
# admin 账号密码或 Access Token
# NEXUS_GITEA_TOKEN='你的密码或令牌'
# 新机安装勿填下面三项(install-nexus-fresh / --fresh 会自动按机生成)
# NEXUS_SECRET_KEY=''
# NEXUS_API_KEY=''
# NEXUS_ENCRYPTION_KEY=''
NEXUS_DOMAIN='api.synaglobal.vip'
NEXUS_ROOT='/opt/nexus'
NEXUS_PROFILE='2c8g'
NEXUS_FROM_ENV='/www/wwwroot/api.synaglobal.vip/.env'
# 本机生产 SSHdeploy-production.sh 读取;私钥路径勿提交真实 .pem)
# NEXUS_SSH='azureuser@20.24.218.235'
# NEXUS_SSH_KEY='/path/to/nz-admin.pem'
+1359
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -61,7 +61,7 @@ server {
location ~ /\. {
deny all;
}
location ~ /data/config\.php$ {
location ~ /data/config\.json$ {
deny all;
}
@@ -75,5 +75,5 @@ server {
error_log {{LOG_DIR}}/{{SERVER_NAME}}_nexus.error.log;
# ── Upload limit ──
client_max_body_size 100m;
client_max_body_size 500m;
}
+14 -2
View File
@@ -53,8 +53,12 @@ server {
proxy_connect_timeout 10s;
}
# WebSocket access log: omit query string so JWT is not written to disk
log_format nexus_ws '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent';
# ── WebSocket proxy (alerts + WebSSH terminal) ──
location /ws/ {
access_log {{LOG_DIR}}/{{SERVER_NAME}}.ws.log nexus_ws;
proxy_pass http://nexus_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
@@ -76,6 +80,8 @@ server {
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 7d;
add_header Cache-Control "public, immutable";
# Must re-declare HSTS — nginx location-level add_header replaces ALL parent add_header directives
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
try_files $uri =404;
}
@@ -83,10 +89,16 @@ server {
location ~ /\. {
deny all;
}
location ~ /data/config\.php$ {
location ~ /data/config\.json$ {
deny all;
}
# ── Redirect bare page paths to /app/ (for reverse-proxy deployments) ──
# If you use proxy_pass instead of root+try_files, uncomment this block:
# location ~ ^/(login|index|servers|files|push|scripts|script-executions|credentials|schedules|retries|commands|alerts|audit|settings|install|terminal|assets)\.html$ {
# return 301 /app/$1.html;
# }
# ── Static HTML pages (no SPA — each page is standalone) ──
location / {
try_files $uri $uri/ =404;
@@ -97,5 +109,5 @@ server {
error_log {{LOG_DIR}}/{{SERVER_NAME}}.error.log;
# ── Upload limit ──
client_max_body_size 100m;
client_max_body_size 500m;
}
Executable
+452
View File
@@ -0,0 +1,452 @@
#!/usr/bin/env bash
# Nexus NX — 统一运维菜单(安装 + 升级 + 日常运维)
#
# nx 交互菜单
# nx update 一键更新(等同 deploy/update.sh
# nx update --no-cache 无缓存重建镜像
# nx install-fresh 全新安装
# nx health 健康检查
#
set -euo pipefail
NX_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
NX_DIR="$(cd "$(dirname "$NX_SELF")" && pwd)"
# shellcheck source=./nexus-1panel.sh
source "${NX_DIR}/nexus-1panel.sh"
pause_enter() {
echo ""
read -r -p "按 Enter 继续..."
}
clear_screen() {
if [[ -t 1 ]]; then
clear 2>/dev/null || true
fi
}
is_stack_installed() {
[[ -d "${NEXUS_ROOT}/.git" && -f "${NEXUS_ROOT}/${ENV_PROD}" ]]
}
detect_runtime() {
if is_stack_installed && docker compose version >/dev/null 2>&1; then
local prof q
prof="$(profile_env_file "$NEXUS_ROOT")"
if [[ -f "$prof" && -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
local -a det_args=(-f "$COMPOSE_FILE")
if has_1panel_network && [[ -f "$NEXUS_ROOT/$COMPOSE_1PANEL" ]]; then
det_args+=(-f "$COMPOSE_1PANEL")
fi
q="$(cd "$NEXUS_ROOT" && docker compose "${det_args[@]}" \
--env-file "$ENV_PROD" --env-file "$prof" ps -q nexus 2>/dev/null || true)"
if [[ -n "$q" ]]; then
NX_RUNTIME="docker"
return 0
fi
fi
fi
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
NX_RUNTIME="supervisor"
return 0
fi
NX_RUNTIME="none"
}
install_nx_cli() {
NEXUS_ROOT="$(cd "${NX_DIR}/.." && pwd)" \
bash "${NX_DIR}/install-nx-cli.sh" 2>/dev/null || true
}
menu_header() {
local title="$1"
clear_screen
echo ""
echo "╔══════════════════════════════════════════════════════════╗"
printf "║ Nexus NX — %-44s║\n" "$title"
echo "╠══════════════════════════════════════════════════════════╣"
echo "║ 域名: ${NEXUS_DOMAIN} 目录: ${NEXUS_ROOT}"
if is_stack_installed; then
detect_runtime
echo "║ 运行: ${NX_RUNTIME} 档位: $(cat "${NEXUS_ROOT}/${HOST_PROFILE_FILE}" 2>/dev/null || echo "${NEXUS_PROFILE}")"
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
echo "║ 版本: $(git -C "${NEXUS_ROOT}" log -1 --oneline 2>/dev/null || echo '?')"
fi
else
echo "║ 状态: 未安装"
fi
echo "╚══════════════════════════════════════════════════════════╝"
echo ""
}
run_install() {
local profile="${1:-2c8g}" extra="${2:-}"
NEXUS_PROFILE="$profile"
SKIP_CLONE=false
FRESH_INSTALL=false
NO_BACKUP=false
PRUNE_IMAGES=false
FROM_ENV="${NEXUS_FROM_ENV}"
case "$extra" in
skip-clone) SKIP_CLONE=true ;;
fresh) FRESH_INSTALL=true ;;
from-env) FROM_ENV="${NEXUS_FROM_ENV}" ;;
esac
load_secrets
cmd_install
install_nx_cli
pause_enter
}
menu_install() {
while true; do
menu_header "安装"
cat <<'EOF'
[1] 一键安装(默认 2核8G / 2c8g)
[2] 安装 — 4核16G4c16g
[3] 安装 — 1核4G1c4g
[4] 自定义规格(输入 CPU 与内存 GB)
[5] 已 clone 到 /opt/nexus,仅 pull + 启动(--skip-clone
[6] 全新安装(install-nexus-fresh.sh → /app/install.html
[7] 检查端口 / 防火墙(不安装)
[0] 返回
EOF
read -r -p "请选择: " choice
case "$choice" in
1) run_install 2c8g "" ;;
2) run_install 4c16g "" ;;
3) run_install 1c4g "" ;;
4)
read -r -p "CPU 核数: " cpus
read -r -p "内存 GB: " mem
CUSTOM_CPUS="$cpus"
CUSTOM_MEM_GB="$mem"
run_install 2c8g ""
;;
5) run_install 2c8g skip-clone ;;
6)
if [[ -x "${NX_DIR}/install-nexus-fresh.sh" ]]; then
bash "${NX_DIR}/install-nexus-fresh.sh" --profile "${NEXUS_PROFILE:-2c8g}"
else
run_install 2c8g fresh
fi
;;
7)
require_root
load_secrets
check_ports_preflight "$NEXUS_ROOT"
if is_stack_installed; then
check_ports_post "$NEXUS_ROOT"
fi
pause_enter
;;
0) return 0 ;;
*) warn "无效选项" ; sleep 1 ;;
esac
done
}
ops_restart_nexus() {
step "重启 NexusPython..."
if [[ "$NX_RUNTIME" == "docker" ]]; then
compose_cmd "$NEXUS_ROOT" restart nexus
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
supervisorctl restart nexus
else
error "未检测到运行中的 Nexus"
return 1
fi
sleep 3
verify_health "$NEXUS_ROOT" "重启" || true
}
ops_restart_stack() {
step "重启 Compose 栈..."
compose_cmd "$NEXUS_ROOT" restart
sleep 5
verify_health "$NEXUS_ROOT" "重启" || true
}
ops_stop_stack() {
warn "将停止 Nexus 容器(MySQL/Redis 为外置服务,不受影响)"
read -r -p "确认? [y/N]: " c
[[ "$c" == "y" || "$c" == "Y" ]] || return 0
compose_cmd "$NEXUS_ROOT" stop
info "已停止"
}
ops_start_stack() {
compose_cmd "$NEXUS_ROOT" up -d --remove-orphans
verify_nexus_1panel_network "$NEXUS_ROOT" || return 1
verify_health "$NEXUS_ROOT" "启动" || true
}
ops_logs() {
local svc="${1:-nexus}"
echo ""
read -r -p "跟踪日志行数 [80]: " n
n="${n:-80}"
compose_cmd "$NEXUS_ROOT" logs --tail="$n" -f "$svc"
}
ops_status() {
if [[ "$NX_RUNTIME" == "docker" ]]; then
compose_cmd "$NEXUS_ROOT" ps
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
supervisorctl status nexus
fi
echo ""
local port h code
port="$(nexus_publish_port "$NEXUS_ROOT")"
h=$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || echo "fail")
info "/health → ${h}"
local cname
cname="$(nexus_container_name 2>/dev/null || true)"
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
if [[ "$code" == "404" ]]; then
info "/app/install.html → 已归档 (HTTP 404)"
else
warn "/app/install.html → 应已归档但 HTTP ${code}"
fi
else
info "/app/install.html → HTTP ${code}"
fi
}
ops_backup_now() {
NO_BACKUP=false
backup_mysql "$NEXUS_ROOT"
}
ops_install_cron() {
bash "${NX_DIR}/install_ops_cron.sh"
}
ops_update() {
local args=()
PRUNE_IMAGES=false
NO_BACKUP=false
NO_CACHE=false
read -r -p "跳过 MySQL 备份? [y/N]: " nb
[[ "$nb" == "y" || "$nb" == "Y" ]] && args+=(--no-backup)
read -r -p "无缓存重建镜像? [y/N]: " nc
[[ "$nc" == "y" || "$nc" == "Y" ]] && args+=(--no-cache)
read -r -p "升级后清理悬空镜像? [y/N]: " pr
[[ "$pr" == "y" || "$pr" == "Y" ]] && args+=(--prune)
bash "${NX_DIR}/update.sh" "${args[@]}"
pause_enter
}
require_installed() {
if ! is_stack_installed; then
warn "尚未安装,请先使用菜单 [1] 安装"
pause_enter
return 1
fi
require_root
load_secrets
load_saved_profile "$NEXUS_ROOT"
detect_runtime
return 0
}
menu_main() {
load_secrets
while true; do
menu_header "运维菜单"
cat <<'EOF'
── 安装 ──
[1] 安装向导(新机 / 迁机 / 档位)
[2] 全新安装 → /app/install.html
── 日常运维(需已安装)──
[3] 状态 + 健康检查
[4] 一键更新(pull + 重建镜像)★
[5] 重启 NexusPython
[6] 查看 Nexus 日志
[7] 启动 / 停止 Compose 栈
[8] 重建镜像并启动(--build nexus
[9] 备份 MySQL
[e] 安装巡检/备份 cronhealth_monitor + db_backup
[v] 1Panel 安装/运维验收脚本
── 其它 ──
[a] 检查 Git 是否有更新
[b] 端口 / 防火墙检查
[c] 进入 Nexus 容器 shell
[d] 1Panel 反代说明
[0] 退出
EOF
read -r -p "请选择: " choice
case "$choice" in
1) menu_install ;;
2)
require_root
if is_stack_installed; then
warn "将执行全新安装(清除 nexus-state 卷内旧配置并重建容器)"
read -r -p "确认继续? [y/N]: " fresh_ok
[[ "$fresh_ok" == "y" || "$fresh_ok" == "Y" ]] || continue
fi
exec bash "${NX_DIR}/install-nexus-fresh.sh" --profile "${NEXUS_PROFILE:-2c8g}"
;;
3)
require_root
if is_stack_installed; then
load_saved_profile "$NEXUS_ROOT"
detect_runtime
ops_status
else
check_ports_preflight "$NEXUS_ROOT"
fi
pause_enter
;;
4)
require_installed || continue
ops_update
;;
5)
require_installed || continue
ops_restart_nexus
pause_enter
;;
6)
require_installed || continue
ops_logs nexus
;;
7)
require_installed || continue
echo " [1] 启动 [2] 停止 [3] 重启栈"
read -r -p "选: " s
case "$s" in
1) ops_start_stack ;;
2) ops_stop_stack ;;
3) ops_restart_stack ;;
*) warn "无效" ;;
esac
pause_enter
;;
8)
require_installed || continue
read -r -p "无缓存重建? [y/N]: " nc
if [[ "$nc" == "y" || "$nc" == "Y" ]]; then
compose_cmd "$NEXUS_ROOT" build --no-cache nexus
fi
compose_cmd "$NEXUS_ROOT" up -d --build --remove-orphans nexus
verify_nexus_1panel_network "$NEXUS_ROOT" || true
verify_health "$NEXUS_ROOT" "重建" || true
pause_enter
;;
9)
require_installed || continue
ops_backup_now
pause_enter
;;
e|E)
require_root
ops_install_cron
pause_enter
;;
v|V)
require_root
bash "${NX_DIR}/verify-1panel-install-wizard.sh"
pause_enter
;;
a|A)
require_root
cmd_check
pause_enter
;;
b|B)
require_root
check_ports_preflight "$NEXUS_ROOT"
is_stack_installed && check_ports_post "$NEXUS_ROOT" || true
pause_enter
;;
c|C)
require_installed || continue
compose_cmd "$NEXUS_ROOT" exec nexus bash || compose_cmd "$NEXUS_ROOT" exec nexus sh
;;
d|D)
info "1Panel → 网站 → 创建站点 ${NEXUS_DOMAIN}"
info "反向代理: http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")"
info "示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example"
pause_enter
;;
0) exit 0 ;;
*) warn "无效选项"; sleep 1 ;;
esac
done
}
nx_main() {
local sub="${1:-}"
install_nx_cli
load_secrets
case "$sub" in
""|menu|god|ops|admin)
# god/ops 已合并进主菜单,保留别名兼容
[[ "$sub" == "god" || "$sub" == "ops" || "$sub" == "admin" ]] && \
warn "「上帝菜单」已合并进 nx 主菜单,直接选对应项即可"
menu_main
;;
install) menu_install ;;
update|upgrade)
require_root
shift || true
exec bash "${NX_DIR}/update.sh" "$@"
;;
install-fresh|fresh)
require_root
exec bash "${NX_DIR}/install-nexus-fresh.sh" "$@"
;;
install-auto|auto)
require_root
if is_stack_installed; then
warn "已安装,install-auto 将执行升级而非重复安装"
exec bash "${NX_DIR}/update.sh"
else
run_install "${NEXUS_PROFILE:-2c8g}" ""
fi
;;
health)
require_root
is_stack_installed && ops_status || check_ports_preflight "$NEXUS_ROOT"
;;
cron)
require_root
ops_install_cron
;;
verify)
require_root
bash "${NX_DIR}/verify-1panel-install-wizard.sh"
;;
-h|--help)
cat <<EOF
用法: nx [子命令]
(无) 统一运维菜单(安装 + 升级 + 重启 + 日志)
update 一键更新(等同 bash deploy/update.sh
install-fresh 全新安装(/app/install.html
install-auto 非交互安装(默认 2c8g)
health 健康检查
cron 安装 health_monitor + db_backup crontab
verify 运行 1Panel 验收脚本
god / ops 已合并进主菜单(兼容别名)
curl 入口:
安装 curl -fsSL .../deploy/quick-install.sh | bash
更新 curl -fsSL .../deploy/update.sh | bash
EOF
;;
*)
main "$@"
;;
esac
}
chmod +x "${NX_DIR}/nx" 2>/dev/null || true
nx_main "$@"
+362
View File
@@ -0,0 +1,362 @@
#!/bin/bash
# Nexus Pre-deploy Gate Check (v3 — 8 gates)
# 在部署前强制检查 8 道门控,任何一道不过则阻止部署
#
# 门控1: CHANGELOG — docs/changelog/ 下必须有今天的 .md 文件(且行数≥10)
# 门控2: AUDIT — docs/audit/ 下必须有今天的审计记录(且包含关键段落)
# 门控3: TEST — tests/test_api.py 必须存在且全通过
# 门控4: LINT — ruff check server/ 零错误
# 门控5: IMPORT — python -c "import server.main" 成功
# 门控6: SECURITY — bandit -r server/ 无 HIGH/MEDIUM
# 门控7: REVIEW — 审计文件包含 Closure表+文件清单+DoD,且文件清单与 git diff 交叉验证
# 门控8: AI_REVIEW — cursor-agent 审查 HEAD~1..HEAD;缓存 docs/reviews/<head_sha>.json
#
# 用法: bash deploy/pre_deploy_check.sh
# 退出码: 0=全部通过, 1=至少一道门未过
# 日志: deploy/gate_log.jsonl(每次检查自动追加记录)
set -euo pipefail
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_REPO_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)"
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
TODAY=$(date +%Y-%m-%d)
TIMESTAMP=$(date -Iseconds)
GATES_PASSED=0
GATES_TOTAL=8
BLOCKED=0
GATE_RESULTS=""
# Resolve tool from .venv / venv / PATH
_pick_bin() {
local name="$1"
for p in "${DEPLOY_DIR}/.venv/bin/${name}" "${DEPLOY_DIR}/venv/bin/${name}"; do
if [ -x "${p}" ]; then
echo "${p}"
return 0
fi
done
command -v "${name}" 2>/dev/null || return 1
}
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
# ── 日志函数 ──
gate_log() {
local gate="$1" result="$2" detail="${3:-}"
local entry="{\"ts\":\"${TIMESTAMP}\",\"date\":\"${TODAY}\",\"gate\":\"${gate}\",\"result\":\"${result}\",\"detail\":\"${detail}\"}"
GATE_RESULTS="${GATE_RESULTS}${entry},"
# 追加到 JSONL 日志文件
echo "${entry}" >> "${DEPLOY_DIR}/deploy/gate_log.jsonl"
}
echo "========================================"
echo " Nexus Pre-deploy Gate Check (v3)"
echo " Date: ${TODAY}"
echo " Gates: ${GATES_TOTAL}"
echo "========================================"
echo ""
# ── Gate 1: Changelog (存在 + 行数≥10) ──
echo -n "Gate 1/8: Changelog ... "
CHANGELOG_DIR="${DEPLOY_DIR}/docs/changelog"
if ls "${CHANGELOG_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
FILE=$(ls -t "${CHANGELOG_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
LINES=$(wc -l < "${FILE}")
if [ "${LINES}" -ge 10 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ Found: $(basename "${FILE}") (${LINES} lines)"
GATES_PASSED=$((GATES_PASSED + 1))
gate_log "changelog" "PASS" "$(basename "${FILE}") ${LINES}lines"
else
echo -e "${RED}BLOCK${NC}"
echo " └─ File exists but only ${LINES} lines (need ≥10)"
echo " └─ Changelog must have substance, not just a placeholder"
BLOCKED=1
gate_log "changelog" "BLOCK" "only ${LINES} lines"
fi
else
echo -e "${RED}BLOCK${NC}"
echo " └─ No changelog file found: ${CHANGELOG_DIR}/${TODAY}-*.md"
echo " └─ Create changelog before deploying"
BLOCKED=1
gate_log "changelog" "BLOCK" "file not found"
fi
# ── Gate 2: Audit (存在 + 关键段落) ──
echo -n "Gate 2/8: Audit ... "
AUDIT_DIR="${DEPLOY_DIR}/docs/audit"
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
# 检查关键段落是否存在
HAS_STEP3=$(grep -c "Step 3" "${FILE}" 2>/dev/null || echo 0)
HAS_CLOSURE=$(grep -c "Closure" "${FILE}" 2>/dev/null || echo 0)
HAS_DOD=$(grep -c "DoD" "${FILE}" 2>/dev/null || echo 0)
if [ "${HAS_STEP3}" -ge 1 ] && [ "${HAS_CLOSURE}" -ge 1 ] && [ "${HAS_DOD}" -ge 1 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ Found: $(basename "${FILE}") (Step3✓ Closure✓ DoD✓)"
GATES_PASSED=$((GATES_PASSED + 1))
gate_log "audit" "PASS" "$(basename "${FILE}")"
else
echo -e "${RED}BLOCK${NC}"
echo " └─ Audit file exists but missing required sections:"
[ "${HAS_STEP3}" -eq 0 ] && echo " └─ Missing: Step 3 (规则扫描)"
[ "${HAS_CLOSURE}" -eq 0 ] && echo " └─ Missing: Closure表"
[ "${HAS_DOD}" -eq 0 ] && echo " └─ Missing: DoD (Definition of Done)"
BLOCKED=1
gate_log "audit" "BLOCK" "missing sections"
fi
else
echo -e "${RED}BLOCK${NC}"
echo " └─ No audit file found: ${AUDIT_DIR}/${TODAY}-*.md"
echo " └─ Run line-walk audit (8 steps) and save report before deploying"
BLOCKED=1
gate_log "audit" "BLOCK" "file not found"
fi
# ── Gate 3: Test (必须存在 + 必须通过) ──
echo -n "Gate 3/8: Test ... "
TEST_SCRIPT="${DEPLOY_DIR}/tests/test_api.py"
if [ ! -f "${TEST_SCRIPT}" ]; then
echo -e "${RED}BLOCK${NC}"
echo " └─ Test script not found: ${TEST_SCRIPT}"
echo " └─ Tests are MANDATORY — cannot deploy without test coverage"
BLOCKED=1
gate_log "test" "BLOCK" "script not found"
else
# Always hit local API for gate 3 (ignore shell NEXUS_TEST_BASE used by import/e2e scripts)
TEST_OUTPUT=$(cd "${DEPLOY_DIR}" && env -u NEXUS_TEST_BASE NEXUS_TEST_BASE=http://127.0.0.1:8600 python3 "${TEST_SCRIPT}" 2>&1) || true
FAIL_COUNT=$(echo "${TEST_OUTPUT}" | grep -cE "^\s+\[FAIL\]" 2>/dev/null || true)
FAIL_COUNT=${FAIL_COUNT:-0}
GATE3_PASSED=0
if [ "${FAIL_COUNT}" -gt 0 ] || echo "${TEST_OUTPUT}" | grep -qE "[0-9]+ test(s) failed"; then
# 本机未起 :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"
GATES_PASSED=$((GATES_PASSED + 1))
gate_log "test" "PASS" "all passed"
fi
fi
# ── Gate 4: Lint (ruff — F only: undefined names, unused imports) ──
echo -n "Gate 4/8: Lint ... "
RUFF_BIN=$(_pick_bin ruff || true)
if [ -n "${RUFF_BIN}" ]; then
LINT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${RUFF_BIN}" check server/ --select F 2>&1) || true
LINT_COUNT=$(echo "${LINT_OUTPUT}" | grep -cE "^server/" 2>/dev/null || true)
LINT_COUNT=$(echo "${LINT_COUNT}" | head -1 | tr -d '[:space:]')
LINT_COUNT=${LINT_COUNT:-0}
if [ "${LINT_COUNT}" -eq 0 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ ruff check: 0 violations"
GATES_PASSED=$((GATES_PASSED + 1))
gate_log "lint" "PASS" "0 violations"
else
echo -e "${RED}BLOCK${NC}"
echo " └─ ruff found ${LINT_COUNT} violations:"
echo "${LINT_OUTPUT}" | head -15 | sed 's/^/ │ /'
BLOCKED=1
gate_log "lint" "BLOCK" "${LINT_COUNT} violations"
fi
else
echo -e "${RED}BLOCK${NC}"
echo " └─ ruff not found (.venv/bin/ruff or pip install ruff)"
BLOCKED=1
gate_log "lint" "BLOCK" "ruff not found"
fi
# ── Gate 5: Import (python -c "import server.main") ──
echo -n "Gate 5/8: Import ... "
IMPORT_PYTHON=""
for p in "${DEPLOY_DIR}/.venv/bin/python3" "${DEPLOY_DIR}/venv/bin/python3"; do
if [ -x "${p}" ]; then
IMPORT_PYTHON="${p}"
break
fi
done
if [ -z "${IMPORT_PYTHON}" ]; then
IMPORT_PYTHON="python3"
fi
IMPORT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${IMPORT_PYTHON}" -c "import server.main" 2>&1) && IMPORT_OK=1 || IMPORT_OK=0
if [ "${IMPORT_OK}" -eq 1 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ server.main imports successfully"
GATES_PASSED=$((GATES_PASSED + 1))
gate_log "import" "PASS" "ok"
else
# 检查是否有真正的导入错误(不是缺少运行时依赖如数据库)
if echo "${IMPORT_OUTPUT}" | grep -qiE "ImportError|ModuleNotFoundError|SyntaxError"; then
echo -e "${RED}BLOCK${NC}"
echo " └─ Import failed:"
echo "${IMPORT_OUTPUT}" | head -5 | sed 's/^/ │ /'
BLOCKED=1
gate_log "import" "BLOCK" "import/syntax error"
else
# 运行时依赖缺失(如数据库连接)不算BLOCK,但警告
echo -e "${YELLOW}WARN${NC}"
echo " └─ Import has runtime dependency issues (not code errors)"
GATES_PASSED=$((GATES_PASSED + 1))
gate_log "import" "WARN" "runtime deps only"
fi
fi
# ── Gate 6: Security (bandit — HIGH only) ──
echo -n "Gate 6/8: Security ... "
BANDIT_BIN=$(_pick_bin bandit || true)
if [ -n "${BANDIT_BIN}" ]; then
SEC_OUTPUT=$(cd "${DEPLOY_DIR}" && "${BANDIT_BIN}" -r server/ -f txt -ll 2>&1) || true
HIGH_COUNT=$(echo "${SEC_OUTPUT}" | grep -cE "Severity: High" 2>/dev/null || true)
HIGH_COUNT=$(echo "${HIGH_COUNT}" | head -1 | tr -d '[:space:]')
HIGH_COUNT=${HIGH_COUNT:-0}
if [ "${HIGH_COUNT}" -eq 0 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ bandit: 0 HIGH findings"
GATES_PASSED=$((GATES_PASSED + 1))
gate_log "security" "PASS" "0 HIGH"
else
echo -e "${RED}BLOCK${NC}"
echo " └─ bandit found ${HIGH_COUNT} HIGH severity issues:"
echo "${SEC_OUTPUT}" | grep -A3 "Severity: High" | head -15 | sed 's/^/ │ /'
BLOCKED=1
gate_log "security" "BLOCK" "${HIGH_COUNT} HIGH findings"
fi
else
echo -e "${RED}BLOCK${NC}"
echo " └─ bandit not found (.venv/bin/bandit or pip install bandit)"
BLOCKED=1
gate_log "security" "BLOCK" "bandit not found"
fi
# ── Gate 7: Review (审计文件交叉验证) ──
echo -n "Gate 7/8: Review ... "
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
# 检查审计文件中是否列出了实际改动的文件
REVIEW_OK=1
# 如果是git仓库,交叉验证改动文件
if command -v git &>/dev/null && [ -d "${DEPLOY_DIR}/.git" ]; then
CHANGED_FILES=$(cd "${DEPLOY_DIR}" && git diff --name-only HEAD~1..HEAD 2>/dev/null || true)
for CF in ${CHANGED_FILES}; do
# 跳过非代码文件与门控运行日志
case "${CF}" in
docs/*|CLAUDE.md|*.md|deploy/gate_log.jsonl|web/app/assets/*) continue ;;
esac
# 检查审计文件是否提到了这个文件
if ! grep -q "$(basename "${CF}")" "${FILE}" 2>/dev/null; then
if [ "${REVIEW_OK}" -eq 1 ]; then
echo -e "${RED}BLOCK${NC}"
REVIEW_OK=0
fi
echo " └─ Changed file not in audit: ${CF}"
fi
done
fi
if [ "${REVIEW_OK}" -eq 1 ]; then
echo -e "${GREEN}PASS${NC}"
echo " └─ Audit covers all changed files"
GATES_PASSED=$((GATES_PASSED + 1))
gate_log "review" "PASS" "audit covers changes"
else
BLOCKED=1
gate_log "review" "BLOCK" "audit missing changed files"
fi
else
echo -e "${RED}BLOCK${NC}"
echo " └─ No audit file to validate against"
BLOCKED=1
gate_log "review" "BLOCK" "no audit file"
fi
# ── Gate 8: AI Review (cursor-agent + docs/reviews cache) ──
echo -n "Gate 8/8: AI Review ... "
AI_REVIEW_OUT=$(cd "${DEPLOY_DIR}" && python3 "${DEPLOY_DIR}/scripts/gate_ai_review.py" 2>&1) || AI_REVIEW_RC=$?
AI_REVIEW_RC=${AI_REVIEW_RC:-0}
if [ "${AI_REVIEW_RC}" -eq 0 ]; then
echo -e "${GREEN}PASS${NC}"
echo "${AI_REVIEW_OUT}" | sed 's/^/ └─ /' | head -5
GATES_PASSED=$((GATES_PASSED + 1))
if echo "${AI_REVIEW_OUT}" | grep -qi "skip"; then
gate_log "ai_review" "PASS" "skipped no code"
else
gate_log "ai_review" "PASS" "ok"
fi
else
echo -e "${RED}BLOCK${NC}"
echo "${AI_REVIEW_OUT}" | sed 's/^/ │ /' | head -12
BLOCKED=1
gate_log "ai_review" "BLOCK" "see gate_ai_review.py output"
fi
# ── Pre-flight: shell scripts LF ──
echo -n "Pre-flight: Shell EOL (LF) ... "
if bash "${DEPLOY_DIR}/deploy/check_shell_eol.sh" >/dev/null 2>&1; then
echo -e "${GREEN}PASS${NC}"
gate_log "shell_eol" "PASS" "all tracked sh LF"
else
echo -e "${RED}BLOCK${NC}"
echo " └─ Run: bash deploy/check_shell_eol.sh"
echo " └─ Fix: git add --renormalize '*.sh'"
BLOCKED=1
gate_log "shell_eol" "BLOCK" "CR in tracked sh"
fi
# ── Pre-flight: deploy-critical paths LF in git index ──
echo -n "Pre-flight: Text EOL (git index) ... "
if python3 "${DEPLOY_DIR}/scripts/check_text_eol.py" >/dev/null 2>&1; then
echo -e "${GREEN}PASS${NC}"
gate_log "text_eol" "PASS" "git index LF"
else
echo -e "${RED}BLOCK${NC}"
echo " └─ Run: python3 scripts/check_text_eol.py --fix-hint"
BLOCKED=1
gate_log "text_eol" "BLOCK" "CR in git index"
fi
# ── Summary ──
echo ""
echo "========================================"
if [ "${BLOCKED}" -eq 0 ]; then
echo -e " Result: ${GREEN}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy allowed"
echo "========================================"
gate_log "summary" "PASS" "${GATES_PASSED}/${GATES_TOTAL}"
exit 0
else
echo -e " Result: ${RED}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy BLOCKED"
echo "========================================"
gate_log "summary" "BLOCK" "${GATES_PASSED}/${GATES_TOTAL}"
exit 1
fi
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env bash
# Nexus production host preflight before applying a release tarball.
#
# Usage:
# bash deploy/preflight-release-host.sh \
# --tarball /tmp/nexus-release-20260708.tar.gz \
# --sha256 a4eb8f35f0b98d40c1ad83117bfc652e7686ffc7d55f6bf90fcfd433a2ad6701 \
# --deploy-path /opt/nexus
set -euo pipefail
TARBALL=""
EXPECTED_SHA256=""
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
MIN_FREE_MB="${NEXUS_RELEASE_MIN_FREE_MB:-1024}"
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
usage() {
sed -n '1,12p' "$0"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--tarball)
TARBALL="${2:-}"
shift 2
;;
--sha256)
EXPECTED_SHA256="${2:-}"
shift 2
;;
--deploy-path)
DEPLOY_PATH="${2:-}"
shift 2
;;
--min-free-mb)
MIN_FREE_MB="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
error "Unknown argument: $1"
usage
exit 2
;;
esac
done
check_cmd() {
local cmd="$1"
if command -v "$cmd" >/dev/null 2>&1; then
info "command OK: $cmd"
return 0
fi
error "missing command: $cmd"
return 1
}
docker_cmd() {
if docker "$@" 2>/dev/null; then return 0; fi
sudo docker "$@" 2>/dev/null
}
detect_runtime() {
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
echo docker
return
fi
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
echo supervisor
return
fi
echo unknown
}
verify_tarball() {
if [[ -z "$TARBALL" ]]; then
warn "--tarball not provided; skipping tarball checksum/content checks"
return 0
fi
[[ -f "$TARBALL" ]] || {
error "tarball not found: $TARBALL"
return 1
}
info "tarball exists: $TARBALL"
if [[ -n "$EXPECTED_SHA256" ]]; then
local actual
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
error "SHA256 mismatch: expected=$EXPECTED_SHA256 actual=$actual"
return 1
fi
info "SHA256 OK: $actual"
else
warn "--sha256 not provided; checksum not verified"
fi
tar tzf "$TARBALL" nexus-release/server nexus-release/deploy nexus-release/web/app-v2 >/dev/null
info "tarball required paths OK"
}
check_deploy_path() {
if [[ -d "$DEPLOY_PATH" ]]; then
info "deploy path exists: $DEPLOY_PATH"
else
warn "deploy path does not exist yet: $DEPLOY_PATH"
fi
if [[ -f "${DEPLOY_PATH}/.env" ]]; then
info "runtime .env preserved path exists"
else
warn "${DEPLOY_PATH}/.env not found"
fi
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]]; then
info "docker/.env.prod exists"
else
warn "${DEPLOY_PATH}/docker/.env.prod not found"
fi
if [[ -d "${DEPLOY_PATH}/web/data" ]]; then
info "web/data exists"
else
warn "${DEPLOY_PATH}/web/data not found"
fi
}
check_disk_space() {
local target free_mb
target="$DEPLOY_PATH"
[[ -e "$target" ]] || target="$(dirname "$DEPLOY_PATH")"
free_mb="$(df -Pm "$target" | awk 'NR==2 {print $4}')"
if [[ -z "$free_mb" ]]; then
warn "unable to determine free disk space for $target"
return 0
fi
if (( free_mb < MIN_FREE_MB )); then
error "low disk space on $target: ${free_mb}MB < ${MIN_FREE_MB}MB"
return 1
fi
info "disk free OK: ${free_mb}MB >= ${MIN_FREE_MB}MB"
}
check_runtime() {
local runtime
runtime="$(detect_runtime)"
info "runtime detected: $runtime"
case "$runtime" in
docker)
docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' >/dev/null \
&& info "Nexus docker container found" \
|| warn "Nexus docker container not found by known name pattern"
;;
supervisor)
supervisorctl status nexus || true
;;
*)
warn "runtime unknown; apply script may require manual restart"
;;
esac
}
check_local_endpoint() {
local port health app app_v2
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
port="${port:-8600}"
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
echo " current /health -> ${health:-<empty>}"
echo " current /app/ -> ${app}"
echo " current /app-v2/ -> ${app_v2}"
}
main() {
local failures=0
for cmd in tar sha256sum df awk grep sed curl; do
check_cmd "$cmd" || failures=$((failures + 1))
done
check_cmd rsync || failures=$((failures + 1))
verify_tarball || failures=$((failures + 1))
check_deploy_path
check_disk_space || failures=$((failures + 1))
check_runtime
check_local_endpoint
if (( failures > 0 )); then
error "preflight failed: ${failures} blocking issue(s)"
return 1
fi
info "preflight OK"
}
main "$@"
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Remove orphaned files under web/app/assets/ not referenced by current Vite build.
Vite/Rolldown minified bundles reference chunks by bare filename (e.g. ServersPage-xxx.js)
without import() paths — scan file contents for known asset basenames.
Orphans newer than --retention-days are kept so long-lived browser tabs can still load
their lazy chunks after a deploy (see docs/design/specs/2026-06-02-keep-session-no-refresh-design.md).
"""
from __future__ import annotations
import re
import sys
import time
from pathlib import Path
# Leading `_` required for Vite chunks like `_plugin-vue_export-helper-*.js`
ASSET_NAME_RE = re.compile(
r"[_A-Za-z0-9][A-Za-z0-9_.-]*\.(?:js|css|woff2?|ttf|eot)"
)
URL_RE = re.compile(r"""url\(["']?([^"')]+)["']?\)""")
DEFAULT_RETENTION_DAYS = 14
def _parse_args(argv: list[str]) -> tuple[bool, int, Path]:
dry_run = "--dry-run" in argv
retention_days = DEFAULT_RETENTION_DAYS
positional: list[str] = []
i = 0
while i < len(argv):
arg = argv[i]
if arg == "--dry-run":
i += 1
continue
if arg.startswith("--retention-days="):
retention_days = int(arg.split("=", 1)[1])
i += 1
continue
if arg == "--retention-days":
if i + 1 >= len(argv):
raise SystemExit("--retention-days requires a value")
retention_days = int(argv[i + 1])
i += 2
continue
positional.append(arg)
i += 1
if retention_days < 0:
raise SystemExit("--retention-days must be >= 0")
app_dir = Path(positional[0] if positional else "/www/wwwroot/api.synaglobal.vip/web/app")
return dry_run, retention_days, app_dir
def _referenced_basenames(text: str, known: set[str]) -> set[str]:
found: set[str] = set()
for m in ASSET_NAME_RE.finditer(text):
name = m.group(0)
if name in known:
found.add(name)
for raw in URL_RE.findall(text):
name = Path(raw).name
if name in known:
found.add(name)
return found
def collect(app_dir: Path) -> set[Path]:
assets = app_dir / "assets"
index = app_dir / "index.html"
if not index.is_file():
raise SystemExit(f"missing {index}")
all_names = {p.name for p in assets.iterdir() if p.is_file()}
keep_names: set[str] = set()
for m in re.finditer(r"/app/assets/([^\s\"']+)", index.read_text(encoding="utf-8")):
name = m.group(1).split("/")[-1]
if name in all_names:
keep_names.add(name)
if not keep_names:
raise SystemExit("no entry assets found in index.html")
changed = True
while changed:
changed = False
for name in list(keep_names):
path = assets / name
if not path.is_file():
continue
text = path.read_text(encoding="utf-8", errors="ignore")
for ref in _referenced_basenames(text, all_names):
if ref not in keep_names:
keep_names.add(ref)
changed = True
return {assets / n for n in keep_names}
def _partition_orphans(
orphan: list[Path],
retention_days: int,
) -> tuple[list[Path], list[Path]]:
"""Split orphans into delete-now vs keep-for-open-tabs (mtime within window)."""
if retention_days <= 0:
return orphan, []
cutoff = time.time() - retention_days * 86400
to_remove: list[Path] = []
retained: list[Path] = []
for p in orphan:
if p.stat().st_mtime >= cutoff:
retained.append(p)
else:
to_remove.append(p)
return to_remove, retained
def main() -> int:
dry_run, retention_days, app_dir = _parse_args(sys.argv[1:])
assets = app_dir / "assets"
if not assets.is_dir():
raise SystemExit(f"not a directory: {assets}")
keep = collect(app_dir)
all_files = {p for p in assets.iterdir() if p.is_file()}
orphan = sorted(all_files - keep, key=lambda p: p.name)
to_remove, retained = _partition_orphans(orphan, retention_days)
if dry_run:
print(
f"dry-run retention_days={retention_days} "
f"referenced_kept={len(keep)} "
f"would_remove={len(to_remove)} "
f"retained_orphans={len(retained)} "
f"total={len(all_files)}"
)
return 0
for p in to_remove:
p.unlink()
print(
f"kept={len(keep)} removed={len(to_remove)} "
f"retained_orphans={len(retained)} retention_days={retention_days} "
f"total_before={len(all_files)}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# =============================================================================
# Nexus 6.0 — 公共仓库一键安装入口(参考 1Panel quick_start.sh
#
# root 登录后一条命令:
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
#
# 或保存后执行:
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" -o /tmp/quick-install.sh
# bash /tmp/quick-install.sh
#
# 可选参数会传给 install-nexus-fresh.sh,例如:
# bash /tmp/quick-install.sh --profile 4c16g
# =============================================================================
set -euo pipefail
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
usage() {
cat <<EOF
Nexus 公共仓库一键安装
curl -fsSL "${NEXUS_RAW_BASE}/deploy/quick-install.sh" | bash
可选参数: --profile 4c16g --domain 你的域名 --skip-1panel-check
EOF
}
validate_script_file() {
local f="$1"
if [[ ! -f "$f" ]] || ! head -1 "$f" | grep -q '^#!/'; then
return 1
fi
if grep -qiE '^(Not found|404)' "$f" 2>/dev/null; then
return 1
fi
return 0
}
download_script() {
local name="$1" dest="$2"
local url base
for base in \
"${NEXUS_RAW_BASE}" \
"${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
url="${base}/deploy/${name}"
if curl -fsSL "$url" -o "$dest" 2>/dev/null && validate_script_file "$dest"; then
info "已下载 ${name}"
return 0
fi
done
return 1
}
run_via_git() {
local url="${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
url="${NEXUS_GITEA_SCHEME}://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
fi
step "改用 git clone..."
mkdir -p "$(dirname "$NEXUS_ROOT")"
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --depth 1 2>/dev/null || \
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --prune
git -C "$NEXUS_ROOT" reset --hard "origin/${NEXUS_GIT_BRANCH}" 2>/dev/null || \
git -C "$NEXUS_ROOT" pull
else
rm -rf "$NEXUS_ROOT"
git clone --depth 1 -b "$NEXUS_GIT_BRANCH" "$url" "$NEXUS_ROOT"
fi
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
exec bash "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" --skip-clone "$@"
}
main() {
if [[ "$(id -u)" -ne 0 ]]; then
error "请使用 root 执行(su - 后直接运行,不要用 sudo 包一层)"
exit 1
fi
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
done
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
echo ""
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}${NC} Nexus 6.0 — 公共仓库一键安装 ${BLUE}${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
case "${1:-}" in
-h|--help) usage; exit 0 ;;
esac
local tmp
tmp="$(mktemp /tmp/install-nexus-fresh.XXXXXX.sh)"
if download_script "install-nexus-fresh.sh" "$tmp"; then
chmod +x "$tmp"
exec bash "$tmp" "$@"
fi
step "raw 下载失败,尝试 git clone..."
run_via_git "$@"
}
# curl | bash 时 stdin 脚本无 BASH_SOURCE[0],不可配合 set -u 做入口判断
main "$@"
+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})"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# Run tests/test_api.py on the deploy host (reads NEXUS_TEST_* from .env).
set -euo pipefail
cd /www/wwwroot/api.synaglobal.vip
export NEXUS_TEST_BASE="${NEXUS_TEST_BASE:-http://127.0.0.1:8600}"
python3 tests/test_api.py
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Sync install wizard static files into the running Nexus prod container.
# Primary fix: Dockerfile.prod bakes these into the image on build.
# This script is a safety net after upgrade if an old image is still running.
set -euo pipefail
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
CONTAINER="${NEXUS_CONTAINER:-}"
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
resolve_container() {
if [[ -n "$CONTAINER" ]]; then
return 0
fi
CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
if [[ -z "$CONTAINER" ]]; then
error "未找到运行中的 Nexus 容器(可设 NEXUS_CONTAINER=名称)"
exit 1
fi
info "容器: $CONTAINER"
}
sync_file() {
local src="$1" dest="$2"
if [[ ! -f "$src" ]]; then
error "缺少源文件: $src"
exit 1
fi
docker exec "$CONTAINER" mkdir -p "$(dirname "$dest")"
docker cp "$src" "${CONTAINER}:${dest}"
}
wizard_archived_in_container() {
docker exec "$CONTAINER" test -f /app/.install_locked 2>/dev/null \
|| docker exec "$CONTAINER" test -f /var/lib/nexus/.install_locked 2>/dev/null \
|| docker exec "$CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null
}
main() {
local app="$NEXUS_ROOT/web/app"
local wizard_archived=false
if [[ ! -f "$app/install.html" ]]; then
error "仓库中无 $app/install.html,请先 git pull"
exit 1
fi
resolve_container
if wizard_archived_in_container; then
wizard_archived=true
info "安装已锁定或向导已归档,跳过静态/Python 热同步"
local code
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
if [[ "$code" == "404" ]]; then
info "/app/install.html 已归档 → HTTP 404"
exit 0
fi
warn "/app/install.html 应已归档但返回 HTTP $code"
exit 1
fi
info "同步安装向导静态文件..."
sync_file "$app/install.html" /app/web/app/install.html
sync_file "$app/vendor/tailwindcss-browser.js" /app/web/app/vendor/tailwindcss-browser.js
sync_file "$app/vendor/theme-init.js" /app/web/app/vendor/theme-init.js
sync_file "$app/vendor/theme.css" /app/web/app/vendor/theme.css
sync_file "$app/vendor/alpinejs.min.js" /app/web/app/vendor/alpinejs.min.js
local install_py="$NEXUS_ROOT/server/api/install.py" code
if [[ -f "$install_py" ]]; then
info "同步 install.pyDocker Redis env-check 修复)..."
sync_file "$install_py" /app/server/api/install.py
info "重启 Nexus 容器以加载 Python 变更..."
docker restart "$CONTAINER" >/dev/null
for _ in $(seq 1 30); do
if curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/health" >/dev/null 2>&1; then
break
fi
sleep 2
done
else
warn "未找到 $install_py,跳过 Python 热同步"
fi
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
if [[ "$code" == "200" ]]; then
info "/app/install.html → HTTP 200"
else
warn "/app/install.html → HTTP $code(请检查 Nexus 是否在 ${NEXUS_PUBLISH_PORT} 监听)"
exit 1
fi
}
main "$@"
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Sync Git-tracked web/app and web/app-v2 into the running Nexus Docker container.
#
# Root cause: Dockerfile.prod runs vite build inside the image; Docker layer cache
# can produce asset hashes that differ from committed web/app on the host.
#
# Usage (on production host after git pull / upgrade):
# NEXUS_ROOT=/opt/nexus bash deploy/sync_webapp_to_container.sh
#
# Called automatically from deploy/nexus-1panel.sh cmd_upgrade and deploy-production.sh.
set -euo pipefail
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
CONTAINER="${NEXUS_CONTAINER:-}"
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
docker_cmd() {
if docker "$@" 2>/dev/null; then return 0; fi
sudo docker "$@" 2>/dev/null
}
resolve_container() {
if [[ -n "$CONTAINER" ]]; then
return 0
fi
CONTAINER="$(docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
if [[ -z "$CONTAINER" ]]; then
error "未找到运行中的 Nexus 容器(可设 NEXUS_CONTAINER=名称)"
exit 1
fi
info "容器: $CONTAINER"
}
verify_entry_bundle() {
local prefix html bundle_path code
prefix="$1"
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${prefix}/" 2>/dev/null || true)"
bundle_path="$(printf '%s' "$html" | grep -oE "${prefix}"'/assets/index-[^"]+\.js' | head -1 || true)"
if [[ -z "$bundle_path" ]]; then
error "${prefix}/ index.html 未解析到主 bundle"
exit 1
fi
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${bundle_path}" 2>/dev/null || echo 000)"
if [[ "$code" != "200" ]]; then
error "主 bundle ${bundle_path} → HTTP ${code}"
exit 1
fi
info "入口校验 OK: ${bundle_path} → HTTP 200"
}
main() {
local app="${NEXUS_ROOT}/web/app"
local app_v2="${NEXUS_ROOT}/web/app-v2"
if [[ ! -f "${app}/index.html" ]]; then
error "缺少 ${app}/index.html,请先 git pull"
exit 1
fi
if [[ ! -d "${app}/assets" ]]; then
error "缺少 ${app}/assets"
exit 1
fi
if [[ ! -f "${app_v2}/index.html" ]]; then
error "缺少 ${app_v2}/index.html,请先构建 frontend-v2"
exit 1
fi
if [[ ! -d "${app_v2}/assets" ]]; then
error "缺少 ${app_v2}/assets"
exit 1
fi
resolve_container
info "同步 web/app → 容器 /app/web/app ..."
docker_cmd exec "$CONTAINER" rm -rf /app/web/app/assets
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app
docker_cmd cp "${app}/index.html" "${CONTAINER}:/app/web/app/index.html"
docker_cmd cp "${app}/assets" "${CONTAINER}:/app/web/app/assets"
if [[ -f "${app}/favicon.ico" ]]; then
docker_cmd cp "${app}/favicon.ico" "${CONTAINER}:/app/web/app/favicon.ico" 2>/dev/null || true
fi
info "同步 web/app-v2 → 容器 /app/web/app-v2 ..."
docker_cmd exec "$CONTAINER" rm -rf /app/web/app-v2/assets
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app-v2
docker_cmd cp "${app_v2}/index.html" "${CONTAINER}:/app/web/app-v2/index.html"
docker_cmd cp "${app_v2}/assets" "${CONTAINER}:/app/web/app-v2/assets"
if [[ -x "${NEXUS_ROOT}/deploy/prune_frontend_assets.py" ]]; then
info "容器内 prune 未引用 assets(可选)..."
docker_cmd exec "$CONTAINER" python3 /app/deploy/prune_frontend_assets.py --dry-run 2>/dev/null || \
warn "容器内 prune 跳过(脚本或路径不可用)"
fi
verify_entry_bundle "/app"
verify_entry_bundle "/app-v2"
info "web/app 与 web/app-v2 同步完成"
}
main "$@"
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Diagnose 1Panel Redis auth from Nexus container network perspective.
# Usage:
# REDIS_PASS='redis_WP3NyN' bash deploy/test-1panel-redis.sh
# bash deploy/test-1panel-redis.sh # prompts for password
set -euo pipefail
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
ENV_PROD="${NEXUS_ROOT}/docker/.env.prod"
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
ok() { echo -e "\033[0;32m[OK]\033[0m $*"; }
fail() { echo -e "\033[0;31m[FAIL]\033[0m $*"; }
pick_redis_container() {
if [[ -n "${REDIS_CONTAINER:-}" ]]; then
echo "$REDIS_CONTAINER"
return 0
fi
local h
h="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
if [[ -n "$h" ]]; then
echo "$h"
return 0
fi
if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then
h="$("$NEXUS_ROOT/deploy/detect-1panel-services.sh" 2>/dev/null | sed -n 's/^NEXUS_1PANEL_REDIS_HOST=//p' | head -1)"
[[ -n "$h" ]] && echo "$h" && return 0
fi
docker ps --format '{{.Names}}' | grep -iE '^1Panel-redis-' | head -1 || true
}
try_ping() {
local label="$1" url="$2"
local nexus
nexus="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
if [[ -z "$nexus" ]]; then
fail "未找到 Nexus 容器"
return 1
fi
if docker exec "$nexus" python3 -c "
import redis
r = redis.Redis.from_url('${url}', socket_timeout=3)
print(r.ping())
" 2>/dev/null | grep -q True; then
ok "$label$url"
return 0
fi
fail "$label"
return 1
}
main() {
local redis_host pass
redis_host="$(pick_redis_container)"
[[ -n "$redis_host" ]] || { fail "未找到 Redis 容器/主机名"; exit 1; }
info "Redis 主机: $redis_host"
if [[ -z "${REDIS_PASS:-}" ]]; then
read -r -s -p "1Panel Redis 密码(应用参数): " pass
echo ""
else
pass="$REDIS_PASS"
fi
echo ""
info "从 Nexus 容器内尝试多种 URL 格式(807f4c2 修复后)..."
local any=0
try_ping "无认证" "redis://${redis_host}:6379/0" && any=1 || true
if [[ -n "$pass" ]]; then
try_ping "仅密码 (:pass@)" "redis://:${pass}@${redis_host}:6379/0" && any=1 || true
try_ping "default 用户" "redis://default:${pass}@${redis_host}:6379/0" && any=1 || true
info "跳过 root 用户探测 — 1Panel Redis 无 root 账号"
fi
echo ""
if [[ "$any" -eq 1 ]]; then
ok "至少一种格式可用"
echo ""
info "安装向导步骤 31Panel Redis 无账号,只填密码(后端自动用 :pass@ 或 default"
info "已有 .env 时写入 NEXUS_REDIS_URL,例如:"
info " redis://:${pass}@${redis_host}:6379/0"
info " 或 redis://default:${pass}@${redis_host}:6379/0"
exit 0
fi
fail "全部失败。请在 1Panel → Redis → 参数 核对密码,或检查 Nexus 是否在 1panel-network"
info "本机: docker exec -it ${redis_host} redis-cli -u 'redis://:密码@127.0.0.1:6379' ping"
exit 1
}
main "$@"
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# =============================================================================
# 卸载 Nexus 旧版 Compose 内置 MySQL 容器
#
# bash deploy/uninstall-mysql-compose.sh
# bash deploy/uninstall-mysql-compose.sh --purge-volume
# =============================================================================
set -euo pipefail
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
PURGE_VOLUME=false
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
usage() {
cat <<EOF
卸载 Nexus Compose 遗留 MySQL 容器
bash deploy/uninstall-mysql-compose.sh [--purge-volume]
说明:
- 不卸载宿主机/1Panel 自建的 MySQL
- 卸载后请自建库 nexus + 用户,安装向导步骤 3 填写连接信息
EOF
}
compose_env_args() {
local_env=()
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
local prof="${NEXUS_ROOT}/docker/.host-profile"
if [[ -f "$prof" ]]; then
local p
p="$(tr -d '\r\n' <"$prof")"
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--purge-volume) PURGE_VOLUME=true; shift ;;
*) error "未知参数: $1"; usage; exit 1 ;;
esac
done
[[ "$(id -u)" -eq 0 ]] || { error "请使用 root 执行"; exit 1; }
removed=0
stop_rm_container() {
local name="$1"
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
info "停止并删除容器: $name"
docker stop "$name" >/dev/null 2>&1 || true
docker rm -f "$name" >/dev/null 2>&1 || true
removed=$((removed + 1))
fi
}
for pattern in nexus-prod-mysql-1 nexus-prod-mysql nexus-mysql-1; do
stop_rm_container "$pattern"
done
while IFS= read -r c; do
[[ -z "$c" ]] && continue
stop_rm_container "$c"
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
compose_env_args
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]] && grep -q '^ mysql:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
warn "compose 仍含 mysql 服务,尝试 compose rm..."
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop mysql 2>/dev/null || true)
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f mysql 2>/dev/null || true)
removed=$((removed + 1))
elif [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" && -f "${NEXUS_ROOT}/docker/.env.prod" ]]; then
info "Compose --remove-orphans(从 nexus-prod 项目移除 mysql 孤儿容器)..."
compose_files=(-f "${NEXUS_ROOT}/${COMPOSE_FILE}")
if docker network inspect 1panel-network >/dev/null 2>&1 \
&& [[ -f "${NEXUS_ROOT}/docker/docker-compose.1panel.yml" ]]; then
compose_files+=(-f "${NEXUS_ROOT}/docker/docker-compose.1panel.yml")
fi
(cd "$NEXUS_ROOT" && docker compose "${compose_files[@]}" "${local_env[@]}" up -d --remove-orphans 2>/dev/null || true)
while IFS= read -r c; do
[[ -z "$c" ]] && continue
stop_rm_container "$c"
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
fi
if [[ "$PURGE_VOLUME" == true ]]; then
while IFS= read -r vol; do
[[ -z "$vol" ]] && continue
info "删除数据卷: $vol"
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol"
done < <(docker volume ls -q | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
fi
leftover="$(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)"
if [[ -n "$leftover" ]]; then
warn "仍有 MySQL 相关容器:"
echo "$leftover" | sed 's/^/ /'
warn "若确认为 Compose 遗留,可: docker rm -f <容器名>"
elif [[ "$removed" -gt 0 ]]; then
info "Compose MySQL 容器已卸载。"
else
info "未发现 Nexus Compose MySQL 容器。"
fi
echo ""
info "当前 Nexus 栈:"
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | grep -i nexus || docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
echo ""
info "下一步:1Panel 应用商店安装 MySQL → nx update 探测容器名 → 向导步骤 3 使用 1Panel-mysql-xxxx"
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# =============================================================================
# 卸载 Nexus 旧版 Compose 内置 Redis 容器
#
# 适用:已从 docker-compose.prod.yml 移除 redis 服务,改用宿主机/1Panel 自建 Redis。
# 不会卸载 apt/1Panel 安装的 Redis,仅删除 Docker 容器与(可选)数据卷。
#
# bash deploy/uninstall-redis-compose.sh
# bash deploy/uninstall-redis-compose.sh --purge-volume # 同时删 redis-data 卷
# =============================================================================
set -euo pipefail
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
PURGE_VOLUME=false
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
usage() {
cat <<EOF
卸载 Nexus Compose 遗留 Redis 容器
bash deploy/uninstall-redis-compose.sh [--purge-volume]
选项:
--purge-volume 删除 nexus-prod_redis-data 数据卷(不可恢复)
-h, --help 显示帮助
说明:
- 不卸载宿主机 redis-server / 1Panel 应用商店 Redis
- 卸载后请在宿主机安装 Redis,安装向导步骤 2 须能 ping 通
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--purge-volume) PURGE_VOLUME=true; shift ;;
*) error "未知参数: $1"; usage; exit 1 ;;
esac
done
if [[ "$(id -u)" -ne 0 ]]; then
error "请使用 root 执行"
exit 1
fi
removed=0
stop_rm_container() {
local name="$1"
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
info "停止并删除容器: $name"
docker stop "$name" >/dev/null 2>&1 || true
docker rm "$name" >/dev/null 2>&1 || true
removed=$((removed + 1))
fi
}
# 常见 Compose 命名
for pattern in nexus-prod-redis-1 nexus-prod-redis nexus-redis-1; do
stop_rm_container "$pattern"
done
# 名称含 nexus 且含 redis 的容器
while IFS= read -r c; do
[[ -z "$c" ]] && continue
stop_rm_container "$c"
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*redis|redis.*nexus' || true)
# 若旧 compose 仍定义 redis 服务
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]]; then
if grep -q '^ redis:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
warn "compose 仍含 redis 服务,尝试 compose rm..."
local_env=()
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
prof="${NEXUS_ROOT}/docker/.host-profile"
if [[ -f "$prof" ]]; then
p="$(tr -d '\r\n' <"$prof")"
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
fi
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop redis 2>/dev/null || true)
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f redis 2>/dev/null || true)
removed=$((removed + 1))
fi
fi
if [[ "$PURGE_VOLUME" == true ]]; then
while IFS= read -r vol; do
[[ -z "$vol" ]] && continue
info "删除数据卷: $vol"
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol(可能被占用)"
done < <(docker volume ls -q | grep -iE 'nexus.*redis|redis.*nexus' || true)
fi
echo ""
if [[ "$removed" -gt 0 ]]; then
info "Compose Redis 容器已卸载。"
else
info "未发现 Nexus Compose Redis 容器(可能已删除)。"
fi
echo ""
info "下一步:1Panel 应用商店安装 Redis → nx update 写入 NEXUS_1PANEL_REDIS_HOST"
echo " 向导 Redis URLredis://:密码@1Panel-redis-xxxx:6379/0(无 root 用户)"
echo " 诊断:bash deploy/test-1panel-redis.sh"
+145
View File
@@ -0,0 +1,145 @@
#!/bin/bash
# ================================================================
# Nexus 6.0 — Uninstall Script
#
# Usage:
# sudo bash uninstall.sh # interactive
# sudo bash uninstall.sh --deploy-dir /opt/nexus # specify deploy dir
# sudo bash uninstall.sh --purge # also remove deploy dir
#
# Safe: stops services, removes configs. Database is NEVER deleted automatically.
# ================================================================
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; }
DEPLOY_DIR=""
PURGE=false
while [[ $# -gt 0 ]]; do
case $1 in
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
--purge) PURGE=true; shift ;;
-h|--help)
echo "Usage: sudo bash uninstall.sh [--deploy-dir PATH] [--purge]"
echo ""
echo " --deploy-dir PATH Specify Nexus installation directory"
echo " --purge Also remove the deploy directory and venv"
echo ""
echo " Note: MySQL database is NEVER deleted automatically."
exit 0 ;;
*) shift ;;
esac
done
# Auto-detect deploy directory
if [ -z "$DEPLOY_DIR" ]; then
for d in /opt/nexus /www/wwwroot/*/; do
if [ -d "$d/server" ] && [ -d "$d/web" ]; then
DEPLOY_DIR="$d"
break
fi
done
fi
if [ -z "$DEPLOY_DIR" ] || [ ! -d "$DEPLOY_DIR/server" ]; then
error "Cannot find Nexus installation. Use --deploy-dir to specify."
exit 1
fi
# Detect BT Panel
BT_MODE=false
if [ -d "/www/server/panel" ] && [ -f "/www/server/panel/class/common.py" ]; then
BT_MODE=true
fi
echo ""
echo "=========================================="
echo " Nexus 6.0 Uninstall"
echo "=========================================="
echo " Deploy: $DEPLOY_DIR"
echo " BT Panel: $BT_MODE"
echo " Purge files: $PURGE"
echo ""
# Confirm
read -rp "Are you sure you want to uninstall Nexus? [y/N] " confirm
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
echo "Cancelled."
exit 0
fi
# 1. Stop Supervisor process
info "Step 1/5: Stopping Nexus process..."
supervisorctl stop nexus >/dev/null 2>&1 || true
# 2. Remove Supervisor config
info "Step 2/5: Removing Supervisor config..."
if [ "$BT_MODE" = true ]; then
SUPERVISOR_CONF="/www/server/panel/plugin/supervisor/profile/nexus.ini"
else
SUPERVISOR_CONF="/etc/supervisor/conf.d/nexus.conf"
fi
if [ -f "$SUPERVISOR_CONF" ]; then
rm -f "$SUPERVISOR_CONF"
supervisorctl reread >/dev/null 2>&1 || true
supervisorctl update >/dev/null 2>&1 || true
info " Removed $SUPERVISOR_CONF"
else
info " Supervisor config not found (already removed)"
fi
# 3. Remove Nginx config
info "Step 3/5: Removing Nginx config..."
if [ "$BT_MODE" = true ]; then
# Find the domain from deploy dir
DOMAIN=$(basename "$DEPLOY_DIR")
NGINX_CONF="/www/server/panel/vhost/nginx/$DOMAIN.conf"
else
NGINX_CONF="/etc/nginx/sites-available/nexus"
NGINX_SYMLINK="/etc/nginx/sites-enabled/nexus"
fi
if [ -f "$NGINX_CONF" ]; then
rm -f "$NGINX_CONF"
[ -L "${NGINX_SYMLINK:-}" ] && rm -f "$NGINX_SYMLINK"
if [ "$BT_MODE" = true ]; then
/www/server/nginx/sbin/nginx -s reload >/dev/null 2>&1 || true
else
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
fi
info " Removed $NGINX_CONF"
else
info " Nginx config not found (already removed)"
fi
# 4. Remove crontab entries
info "Step 4/5: Removing crontab entries..."
crontab -l 2>/dev/null | grep -v "health_monitor.sh" | grep -v "db_backup.sh" | crontab - 2>/dev/null || true
info " Crontab entries removed"
# 5. Optionally remove deploy directory
if [ "$PURGE" = true ]; then
info "Step 5/5: Removing deploy directory (--purge)..."
rm -rf "$DEPLOY_DIR"
info " Removed $DEPLOY_DIR"
else
info "Step 5/5: Keeping deploy directory (use --purge to remove)"
info " To remove manually: rm -rf $DEPLOY_DIR"
fi
echo ""
echo -e " ${GREEN}Nexus uninstalled successfully.${NC}"
echo ""
echo " Note: MySQL database was NOT deleted."
echo " To drop the database manually:"
echo " mysql -u root -e 'DROP DATABASE IF EXISTS nexus;'"
echo ""
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Nexus 6.0 — 一键更新(拉代码 + 重建镜像 + 重启)
#
# 已在 /opt/nexus:
# bash deploy/update.sh
# bash deploy/update.sh --no-cache # 强制无缓存重建(entrypoint/Dockerfile 变更后)
#
# 远程一条命令(推荐在仓库目录执行,管道模式会回退 /opt/nexus:
# cd /opt/nexus && bash deploy/update.sh
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
#
set -euo pipefail
_resolve_nexus_root() {
local src="${BASH_SOURCE[0]:-}"
if [[ -n "$src" && "$src" != /dev/fd/* && "$src" != "-" && -f "$src" ]]; then
cd "$(dirname "$(readlink -f "$src")")/.." && pwd
return
fi
if [[ -d /opt/nexus ]]; then
echo "警告: 通过管道执行无法解析脚本路径,已回退 /opt/nexus;推荐: cd /opt/nexus && bash deploy/update.sh" >&2
echo /opt/nexus
return
fi
echo "错误: 无法定位 Nexus 根目录;请 cd /opt/nexus && bash deploy/update.sh" >&2
exit 1
}
ROOT="$(_resolve_nexus_root)"
NEXUS_ROOT="$ROOT" bash "$ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
exec bash "$ROOT/deploy/nexus-1panel.sh" upgrade "$@"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# 兼容入口 — 请优先使用 deploy/nexus-1panel.sh
exec "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/nexus-1panel.sh" upgrade "$@"
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# Nexus 6.0 — Upgrade Script(自动识别 Docker / Supervisor
#
# sudo bash deploy/upgrade.sh
# sudo bash deploy/upgrade.sh --deploy-dir /opt/nexus
#
# Docker 1Panel:委托 deploy/nexus-1panel.sh upgrade(拉代码 + 备份 + 重建镜像)
# Supervisor 裸机:git pull + venv pip + supervisorctl restart
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./detect_deploy_runtime.sh
source "${SCRIPT_DIR}/detect_deploy_runtime.sh"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; }
DEPLOY_DIR=""
EXTRA_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
--no-backup|--require-backup|--no-cache|--prune|--check|--dry-run)
EXTRA_ARGS+=("$1"); shift ;;
-h|--help)
cat <<'EOF'
用法: sudo bash deploy/upgrade.sh [--deploy-dir PATH] [nexus-1panel 升级选项]
Docker 环境自动走 nexus-1panel.sh upgradeSupervisor 环境走 venv + supervisorctl。
EOF
exit 0
;;
*) EXTRA_ARGS+=("$1"); shift ;;
esac
done
if [[ -z "$DEPLOY_DIR" ]]; then
DEPLOY_DIR="$(resolve_nexus_root 2>/dev/null || true)"
fi
if [[ -z "$DEPLOY_DIR" || ! -d "$DEPLOY_DIR/server" ]]; then
error "找不到 Nexus 安装目录,请使用 --deploy-dir 指定"
exit 1
fi
RUNTIME="$(detect_nexus_runtime "$DEPLOY_DIR")"
if [[ "$RUNTIME" == "docker" ]]; then
info "检测到 Docker Compose 运行时,委托 nexus-1panel.sh upgrade"
exec env NEXUS_ROOT="$DEPLOY_DIR" bash "$DEPLOY_DIR/deploy/nexus-1panel.sh" upgrade "${EXTRA_ARGS[@]}"
fi
VENV_DIR="$DEPLOY_DIR/venv"
ENV_FILE="$DEPLOY_DIR/.env"
echo ""
echo "=========================================="
echo " Nexus 6.0 Upgrade (Supervisor)"
echo "=========================================="
echo " Deploy: $DEPLOY_DIR"
echo ""
info "Step 1/4: Updating source code..."
if [[ -d "$DEPLOY_DIR/.git" ]]; then
cd "$DEPLOY_DIR"
OLD_REV="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
git pull --ff-only 2>/dev/null || {
error "git pull failed. Resolve conflicts: cd $DEPLOY_DIR && git status"
exit 1
}
NEW_REV="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
if [[ "$OLD_REV" == "$NEW_REV" ]]; then
info " Already up to date (no new commits)"
else
info " Updated: ${OLD_REV:0:8}..${NEW_REV:0:8}"
fi
else
warn " Not a git repo. Skip git pull"
fi
info "Step 2/4: Updating Python dependencies..."
if [[ -d "$VENV_DIR/bin" ]]; then
"$VENV_DIR/bin/pip" install -q --upgrade pip
"$VENV_DIR/bin/pip" install -q -r "$DEPLOY_DIR/requirements.txt" 2>/dev/null || {
warn " pip install had warnings, continuing..."
}
else
error " venv not found at $VENV_DIR. Run install.sh first."
exit 1
fi
info "Step 3/4: Restarting Nexus..."
supervisorctl restart nexus >/dev/null 2>&1 || {
warn " supervisorctl restart failed. Try: supervisorctl restart nexus"
}
info "Step 4/4: Verifying health..."
PORT="$(nexus_publish_port_for "$DEPLOY_DIR")"
HEALTH_OK=false
for _ in $(seq 1 15); do
if curl -sf "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
HEALTH_OK=true
break
fi
sleep 2
done
echo ""
if [[ "$HEALTH_OK" == true ]]; then
echo -e " ${GREEN}Upgrade complete! Nexus is healthy.${NC}"
suggest_ops_cron "$DEPLOY_DIR"
else
echo -e " ${YELLOW}Health check pending. Verify manually:${NC}"
echo " curl http://127.0.0.1:${PORT}/health"
echo " supervisorctl status nexus"
fi
echo ""
+437
View File
@@ -0,0 +1,437 @@
#!/usr/bin/env bash
# Verify 1Panel + Docker install wizard readiness (run on the VPS as root).
# See: deploy/README-1panel.md
set -euo pipefail
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
ENV_PROD="${NEXUS_ROOT}/docker/.env.prod"
COMPOSE_FILE="${NEXUS_ROOT}/docker/docker-compose.prod.yml"
COMPOSE_1PANEL="${NEXUS_ROOT}/docker/docker-compose.1panel.yml"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
pass() { echo -e "${GREEN}[PASS]${NC} $*"; }
fail() { echo -e "${RED}[FAIL]${NC} $*"; FAILURES=$((FAILURES + 1)); }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; WARNS=$((WARNS + 1)); }
info() { echo -e " $*"; }
FAILURES=0
WARNS=0
INSTALL_WIZARD_ARCHIVED=false
PUBLISH_PORT=8600
require_root() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
fail "请用 root 运行(1Panel 终端默认 root"
exit 1
fi
}
resolve_nexus_container() {
NEXUS_CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
if [[ -z "$NEXUS_CONTAINER" ]]; then
fail "未找到运行中的 Nexus 容器"
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
return 1
fi
pass "Nexus 容器: $NEXUS_CONTAINER"
}
check_repo() {
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
fail "仓库不存在: $NEXUS_ROOT"
return 1
fi
local sha msg
sha="$(git -C "$NEXUS_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
msg="$(git -C "$NEXUS_ROOT" log -1 --oneline 2>/dev/null || true)"
pass "Git: $msg"
if git -C "$NEXUS_ROOT" merge-base --is-ancestor b25d079 HEAD 2>/dev/null; then
pass "已包含 b25d0791Panel 容器名 sync 修复)"
else
warn "早于 b25d079 — 步骤 3 可能仍预填 host.docker.internal"
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
fi
if git -C "$NEXUS_ROOT" merge-base --is-ancestor 3b2856f HEAD 2>/dev/null; then
pass "已包含 3b2856f+Redis :pass@ 自动解析,无 root"
else
warn "早于 3b2856f — Redis 认证可能仍误用 root"
info "修复: cd $NEXUS_ROOT && git pull && bash deploy/sync-install-wizard-to-container.sh"
fi
}
check_env_prod() {
if [[ ! -f "$ENV_PROD" ]]; then
fail "缺少 $ENV_PROD"
info "修复: bash $NEXUS_ROOT/deploy/install-nexus-fresh.sh --skip-clone"
return 1
fi
pass "docker/.env.prod 存在"
PUBLISH_PORT="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- | tr -d '\r" ' || true)"
PUBLISH_PORT="${PUBLISH_PORT:-8600}"
pass "NEXUS_PUBLISH_PORT=$PUBLISH_PORT"
if grep -qE '^NEXUS_INSTALL_WIZARD_PENDING=1' "$ENV_PROD"; then
pass "NEXUS_INSTALL_WIZARD_PENDING=1(安装模式)"
else
warn "NEXUS_INSTALL_WIZARD_PENDING≠1 — entrypoint 可能已写 /app/.env"
fi
local db redis
db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
if [[ -n "$db" ]]; then
pass "NEXUS_1PANEL_DB_HOST=$db"
else
fail "未设置 NEXUS_1PANEL_DB_HOST"
info "修复: bash $NEXUS_ROOT/deploy/detect-1panel-services.sh && nx update"
fi
if [[ -n "$redis" ]]; then
pass "NEXUS_1PANEL_REDIS_HOST=$redis"
local rurl
rurl="$(grep -E '^NEXUS_REDIS_URL=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
if [[ "$rurl" == *"$redis"* ]]; then
pass "NEXUS_REDIS_URL 已指向 Redis 容器"
else
warn "NEXUS_REDIS_URL 未含 $redis — nx update 会同步"
fi
else
fail "未设置 NEXUS_1PANEL_REDIS_HOST"
info "修复: 在 1Panel 应用商店安装 Redis 后执行 nx update"
fi
}
check_1panel_network() {
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
fail "1panel-network 不存在"
info "修复: 在 1Panel 应用商店安装 MySQL + Redis(会自动创建网络)"
return 1
fi
pass "1panel-network 存在"
if [[ -n "${NEXUS_CONTAINER:-}" ]]; then
if docker inspect "$NEXUS_CONTAINER" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' \
| grep -q '1panel-network'; then
pass "Nexus 已接入 1panel-network"
else
fail "Nexus 未接入 1panel-network"
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
fi
fi
if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then
info "探测结果:"
"$NEXUS_ROOT/deploy/detect-1panel-services.sh" | sed 's/^/ /' || true
fi
}
probe_from_container() {
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
local db redis
db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
if [[ -n "$db" ]]; then
if docker exec "$NEXUS_CONTAINER" python3 -c "
import socket
s=socket.socket(); s.settimeout(3); s.connect(('$db', 3306)); s.close()
" 2>/dev/null; then
pass "容器内 TCP → MySQL $db:3306"
else
fail "容器内无法连接 MySQL $db:3306"
info "检查 MySQL 容器是否运行、Nexus 是否在 1panel-network"
fi
fi
if [[ -n "$redis" ]]; then
if docker exec "$NEXUS_CONTAINER" python3 -c "
import socket
s=socket.socket(); s.settimeout(3); s.connect(('$redis', 6379)); s.close()
" 2>/dev/null; then
pass "容器内 TCP → Redis $redis:6379"
else
fail "容器内无法连接 Redis $redis:6379"
fi
fi
}
check_app_env_redis_url() {
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
if ! docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then
return 0
fi
local rurl
rurl="$(docker exec "$NEXUS_CONTAINER" grep -E '^NEXUS_REDIS_URL=' /app/.env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
if [[ -z "$rurl" ]]; then
warn "/app/.env 无 NEXUS_REDIS_URL"
return 0
fi
if [[ "$rurl" == *"://root:"* ]]; then
fail "NEXUS_REDIS_URL 含 root 用户(1Panel Redis 无 root 账号)"
info "修复: redis://:密码@1Panel-redis-xxxx:6379/0 — 见 deploy/test-1panel-redis.sh"
elif [[ "$rurl" == *"1Panel-redis"* ]] || [[ "$rurl" == *"://:"* ]]; then
pass "NEXUS_REDIS_URL 格式合理(容器名或 :pass@"
else
warn "NEXUS_REDIS_URL 可能未指向 1Panel Redis: $rurl"
fi
}
check_install_mode() {
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
if docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then
warn "/app/.env 已存在 — 安装向导可能已进入步骤 3+ 或误写 env"
check_app_env_redis_url
info "若需重来: 删卷 nexus-state 中 .env 并设 NEXUS_INSTALL_WIZARD_PENDING=1 后 nx update"
else
pass "/app/.env 不存在(安装模式 OK"
fi
if docker exec "$NEXUS_CONTAINER" test -f /app/.install_locked 2>/dev/null \
|| docker exec "$NEXUS_CONTAINER" test -f /var/lib/nexus/.install_locked 2>/dev/null \
|| docker exec "$NEXUS_CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null; then
pass "安装向导已锁定/已归档 — 应访问 /app/ 登录"
INSTALL_WIZARD_ARCHIVED=true
fi
local hosts_json
hosts_json="$(docker exec "$NEXUS_CONTAINER" cat /app/web/data/1panel-hosts.json 2>/dev/null || true)"
if [[ -n "$hosts_json" ]]; then
pass "1panel-hosts.json: $hosts_json"
else
warn "无 1panel-hosts.json(依赖容器环境变量,nx update 会写入)"
fi
}
check_ops_cron() {
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" != true ]]; then
return 0
fi
if ! command -v crontab >/dev/null 2>&1; then
warn "未安装 cron/crontab — Layer 3 宿主机巡检不可用"
info "修复: sudo bash $NEXUS_ROOT/deploy/install_ops_cron.sh"
return 0
fi
local cron
cron="$(crontab -l 2>/dev/null || true)"
if echo "$cron" | grep -q 'nexus-health-monitor\|deploy/health_monitor.sh'; then
pass "Crontab 含 health_monitor"
else
warn "Crontab 未配置 health_monitorLayer 3 巡检)"
info "修复: sudo nx cron"
fi
if echo "$cron" | grep -q 'nexus-mysql-backup\|deploy/db_backup.sh'; then
pass "Crontab 含 db_backup"
else
warn "Crontab 未配置 db_backup(每日 MySQL 备份)"
info "修复: sudo nx cron"
fi
}
check_mysql_backup() {
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" != true ]]; then
return 0
fi
local dump_sh="${NEXUS_ROOT}/deploy/mysql_dump_to_file.sh"
if [[ -f "$dump_sh" ]]; then
pass "mysql_dump_to_file.sh 存在"
else
warn "缺少 mysql_dump_to_file.sh — 升级前无法备份 MySQL"
info "修复: cd $NEXUS_ROOT && git pull && nx update"
return 0
fi
if [[ -d /var/backups/nexus ]] && ls /var/backups/nexus/nexus_*.sql.gz >/dev/null 2>&1; then
local latest
latest="$(ls -t /var/backups/nexus/nexus_*.sql.gz 2>/dev/null | head -1)"
pass "已有 MySQL 备份: $(basename "$latest")"
else
warn "尚无 MySQL 备份文件(/var/backups/nexus"
info "可手动: sudo bash $NEXUS_ROOT/deploy/db_backup.sh"
fi
}
check_http_local() {
local health code
health="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/health" 2>/dev/null || true)"
if [[ "$health" == "ok" ]]; then
pass "/health → ok"
else
fail "/health 非 ok(当前: ${health:-无响应}"
info "修复: docker logs --tail=80 $NEXUS_CONTAINER"
return 1
fi
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
if [[ "$code" == "404" ]]; then
pass "/app/install.html 已归档 → HTTP 404"
else
fail "/app/install.html 应已归档但 HTTP $code"
info "修复: nx update(会执行 archive_install_wizard_in_container"
fi
return 0
fi
if [[ "$code" == "200" ]]; then
pass "/app/install.html → HTTP 200"
else
fail "/app/install.html → HTTP $code"
info "修复: bash $NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh"
fi
}
check_install_api() {
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
pass "安装 API 已关闭(向导已锁定,/api/install/* 返回 403 为预期)"
return 0
fi
local status env_json
status="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/api/install/status" 2>/dev/null || true)"
if [[ -n "$status" ]]; then
pass "/api/install/status → $status"
if echo "$status" | grep -q '"installed":true'; then
warn "installed=true — 向导已完成,请访问 /app/ 登录"
fi
else
fail "/api/install/status 无响应"
fi
env_json="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/api/install/env-check" 2>/dev/null || true)"
if [[ -z "$env_json" ]]; then
fail "/api/install/env-check 无响应"
return 1
fi
if echo "$env_json" | grep -q 'docker_defaults'; then
local db_host
db_host="$(echo "$env_json" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print((d.get('docker_defaults') or {}).get('db_host',''))
" 2>/dev/null || true)"
if [[ -n "$db_host" && "$db_host" != "host.docker.internal" ]]; then
pass "步骤 3 预填 db_host=$db_host"
elif [[ "$db_host" == "host.docker.internal" ]]; then
fail "步骤 3 仍预填 host.docker.internal"
info "修复: nx update --no-cache(需 b25d079+"
fi
else
warn "env-check 无 docker_defaults(非 Docker 模式?)"
fi
if echo "$env_json" | grep -q '1Panel-mysql'; then
pass "env-check MySQL 探测含 1Panel 容器名"
elif echo "$env_json" | grep -q 'host.docker.internal'; then
warn "env-check MySQL 仍指向 host.docker.internal"
fi
}
check_container_image_features() {
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
local host_sha py_ok html_file html_ok host_root_ok
host_sha="$(git -C "$NEXUS_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
pass "宿主机 Git: $host_sha"
if docker exec "$NEXUS_CONTAINER" grep -q '_host_deploy_root' /app/server/api/install.py 2>/dev/null; then
pass "容器 install.py 含 host_deploy_rootround6+"
py_ok=1
else
warn "容器 install.py 较旧 — 需 nx update 重建镜像"
fi
html_file="/app/web/app/install.html"
if docker exec "$NEXUS_CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null; then
html_file="/app/web/app/install.html.bak"
fi
if docker exec "$NEXUS_CONTAINER" grep -q 'install_ops_cron' "$html_file" 2>/dev/null; then
pass "容器 install.html 含 Layer 3 cron 指引(round7+"
html_ok=1
else
warn "容器 install.html 较旧 — 需 nx update 重建镜像"
fi
host_root_ok="$(docker exec "$NEXUS_CONTAINER" printenv NEXUS_HOST_ROOT 2>/dev/null || true)"
if [[ -n "$host_root_ok" ]]; then
pass "NEXUS_HOST_ROOT=$host_root_ok"
else
warn "容器未注入 NEXUS_HOST_ROOT — nx update 重建后可修复"
fi
if [[ -n "${py_ok:-}" && -n "${html_ok:-}" ]]; then
info "镜像与宿主机代码特性对齐(重建后验收)"
else
info "修复: cd $NEXUS_ROOT && sudo nx update"
fi
}
check_openresty_hint() {
local domain code
domain="$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo api.synaglobal.vip)"
code="$(curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 5 "https://${domain}/health" 2>/dev/null || echo 000)"
if [[ "$code" == "200" ]] && curl -sk "https://${domain}/health" 2>/dev/null | grep -q '^ok$'; then
pass "HTTPS https://${domain}/health → ok"
else
warn "HTTPS https://${domain}/health → HTTP $code(外网可能未配 OpenResty 反代)"
info "1Panel → 网站 → 反代 http://127.0.0.1:8600"
info "参考: $NEXUS_ROOT/deploy/1panel/openresty-nexus.conf.example"
fi
}
print_next_steps() {
local domain
domain="$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo '你的域名')"
echo ""
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
echo "=========================================="
echo " 已安装 — 日常运维"
echo "=========================================="
echo " 登录: https://${domain}/app/"
echo " 更新: cd $NEXUS_ROOT && nx update"
echo " 巡检 cron: sudo nx cron"
echo " 手动备份: sudo bash $NEXUS_ROOT/deploy/db_backup.sh"
echo " 验收: bash $NEXUS_ROOT/deploy/verify-1panel-install-wizard.sh"
echo ""
return 0
fi
echo "=========================================="
echo " 安装向导后续步骤"
echo "=========================================="
echo "1. 1Panel 数据库 → 建库 nexus、用户 nexus(仅授权 nexus 库)"
echo "2. 浏览器打开: https://${domain}/app/install.html"
echo "3. 步骤 2MySQL/Redis TCP 应显示 ✓(容器名:端口)"
echo "4. 步骤 3:确认 db_host/redis_host 为 1Panel-mysql/redis-xxxx,填 MySQL 密码"
echo "5. 步骤 4:连接检测全绿 → 创建 admin"
echo "6. 步骤 5:锁定安装"
echo ""
if [[ "$FAILURES" -gt 0 ]]; then
echo "先修复上方 [FAIL],再执行:"
echo " cd $NEXUS_ROOT && nx update --no-cache"
fi
}
main() {
require_root
echo "Nexus 1Panel 安装向导验收 — $(date -Iseconds)"
echo "NEXUS_ROOT=$NEXUS_ROOT"
echo ""
check_repo || true
check_env_prod || true
resolve_nexus_container || true
check_1panel_network || true
probe_from_container || true
check_install_mode || true
check_http_local || true
check_install_api || true
check_ops_cron || true
check_mysql_backup || true
check_container_image_features || true
check_openresty_hint || true
print_next_steps
if [[ "$FAILURES" -gt 0 ]]; then
echo ""
fail "合计 $FAILURES 项未通过"
exit 1
fi
echo ""
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
pass "验收通过 — 安装已完成,请访问 /app/ 登录"
if [[ "$WARNS" -gt 0 ]]; then
warn "${WARNS} 项 [WARN](cron/备份等建议项,不阻断登录)"
fi
else
pass "验收通过 — 可打开安装向导完成步骤 1–5"
fi
}
main "$@"
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Post-deploy smoke: login + optional batch endpoint (run on server)."""
import json
import os
import sys
import urllib.error
import urllib.request
BASE = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600")
USER = os.environ.get("NEXUS_TEST_ADMIN_USER", "admin")
PASSWORD = os.environ.get("NEXUS_TEST_ADMIN_PASSWORD", "")
def post(path: str, body: dict, headers: dict | None = None) -> dict:
h = {"Content-Type": "application/json", **(headers or {})}
req = urllib.request.Request(
f"{BASE}{path}",
data=json.dumps(body).encode(),
headers=h,
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.load(resp)
def main() -> int:
if not PASSWORD:
print("SKIP: set NEXUS_TEST_ADMIN_PASSWORD", file=sys.stderr)
return 2
login = post("/api/auth/login", {"username": USER, "password": PASSWORD})
token = login.get("access_token")
exp = login.get("expires_in")
print(f"login ok expires_in={exp}")
if exp != 3600:
print(f"WARN: expected expires_in=3600, got {exp}")
if not token:
print("FAIL: no access_token")
return 1
# Minimal authenticated call
req = urllib.request.Request(
f"{BASE}/api/servers/stats",
headers={"Authorization": f"Bearer {token}"},
)
with urllib.request.urlopen(req, timeout=30) as resp:
stats = json.load(resp)
print(f"servers/stats ok keys={list(stats.keys())[:5]}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except urllib.error.HTTPError as e:
print(f"HTTP {e.code}: {e.read().decode()[:500]}", file=sys.stderr)
raise SystemExit(1)
+89
View File
@@ -0,0 +1,89 @@
# Nexus 6.0 — Docker Compose (dev / staging)
# Usage:
# cp docker/.env.example docker/.env # or: python docker/generate_env.py
# docker compose up -d --build
# open http://localhost:8600/app/
name: nexus
services:
mysql:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-nexus_root_dev}
MYSQL_DATABASE: nexus
MYSQL_USER: nexus
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-nexus_dev_pass}
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"]
interval: 10s
timeout: 5s
retries: 8
start_period: 40s
ports:
- "${MYSQL_PUBLISH_PORT:-3306}:3306"
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 8
ports:
- "${REDIS_PUBLISH_PORT:-6379}:6379"
nexus:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "${NEXUS_PUBLISH_PORT:-8600}:8600"
environment:
MYSQL_HOST: mysql
MYSQL_PORT: "3306"
REDIS_HOST: redis
REDIS_PORT: "6379"
NEXUS_PERSIST_DIR: /var/lib/nexus
NEXUS_DOCKER_WRITE_ENV: "1"
NEXUS_HOST: "0.0.0.0"
NEXUS_PORT: "8600"
NEXUS_DEPLOY_PATH: /app
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD:-nexus_dev_pass}@mysql:3306/nexus
NEXUS_REDIS_URL: redis://redis:6379/0
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:-http://localhost:8600,http://127.0.0.1:8600}
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:-}
NEXUS_API_KEY: ${NEXUS_API_KEY:-}
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:-}
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:-http://localhost:8600}
env_file:
- path: docker/.env
required: false
volumes:
- nexus-state:/var/lib/nexus
- nexus-web-data:/app/web/data
healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1:8600/health"]
interval: 30s
timeout: 5s
retries: 5
start_period: 90s
volumes:
mysql-data:
redis-data:
nexus-state:
nexus-web-data:
+18
View File
@@ -0,0 +1,18 @@
# Copy to docker/.env (or run: python docker/generate_env.py)
# Used by docker compose for nexus service — never commit docker/.env
MYSQL_ROOT_PASSWORD=change_me_root
MYSQL_PASSWORD=change_me_nexus
# Leave empty to use install wizard at http://localhost:8600/app/install.html
# Or run generate_env.py to fill these three:
NEXUS_SECRET_KEY=
NEXUS_API_KEY=
NEXUS_ENCRYPTION_KEY=
NEXUS_CORS_ORIGINS=http://localhost:8600,http://127.0.0.1:8600
NEXUS_API_BASE_URL=http://localhost:8600
# Optional host port overrides
# NEXUS_PUBLISH_PORT=8600
# MYSQL_PUBLISH_PORT=3306
+39
View File
@@ -0,0 +1,39 @@
# Production Compose env — copy to docker/.env.prod (never commit)
# Fresh install: bash deploy/install-nexus-fresh.sh (auto-generates NEXUS keys per host)
# MySQL / Redis 不在 Docker 栈 — 请在宿主机或 1Panel 自建,安装向导步骤 3 填写
# Auto-generated on fresh install (unique per server). Do NOT copy between hosts.
NEXUS_SECRET_KEY=
NEXUS_API_KEY=
NEXUS_ENCRYPTION_KEY=
# 1 = entrypoint must not write /app/.env until install wizard step 3
NEXUS_INSTALL_WIZARD_PENDING=0
# Public URL
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
NEXUS_API_BASE_URL=https://api.synaglobal.vip
# Nexus container resource profile (docker/profiles/*.env)
NEXUS_DB_POOL_SIZE=30
NEXUS_DB_MAX_OVERFLOW=20
# 1Panelnx update 自动探测并写入(安装向导步骤 3 预填)
# NEXUS_1PANEL_DB_HOST=1Panel-mysql-xxxx
# NEXUS_1PANEL_REDIS_HOST=1Panel-redis-xxxx
# 宿主机 Compose 插值/ nx update 同步用;安装向导完成后以卷内 /app/.env 为准(不经 Compose 注入)
# 1Panel 有密码时: redis://:你的密码@1Panel-redis-xxxx:6379/0
NEXUS_REDIS_URL=redis://host.docker.internal:6379/0
# 宿主机 git 仓库路径(容器内安装向导用于提示 nx cron / nx update
NEXUS_HOST_ROOT=/opt/nexus
# Optional — production probe / E2E only (host .env.prod; NOT for API runtime; never commit values)
# NEXUS_TEST_ADMIN_PASSWORD — must match admin login password
# NEXUS_PROBE_CLIENT_IP — optional; IP in login allowlist for origin probe (auto-read from DB if unset)
# NEXUS_TEST_ADMIN_PASSWORD=
# NEXUS_PROBE_CLIENT_IP=
# Optional
# NEXUS_PUBLISH_PORT=8600
+71
View File
@@ -0,0 +1,71 @@
# Nexus Docker
## 前置
- [Docker Desktop](https://www.docker.com/products/docker-desktop/)Windows/macOS)或 Docker Engine + Compose v2
## 快速启动(正常模式)
```bash
# 1. 生成密钥(写入 docker/.env,已 gitignore
python docker/generate_env.py
# 2. 构建并启动
docker compose up -d --build
# 3. 健康检查
curl -s http://localhost:8600/health # 期望: ok
# 4. 打开 SPA
# http://localhost:8600/app/
```
默认管理员需在安装向导或已有 DB 中配置;若 DB 为空且未跑过 install,可删卷后走安装模式。
## 安装向导模式
不创建 `docker/.env`(或清空 `NEXUS_SECRET_KEY`)时,应用以 **安装模式** 启动:
- http://localhost:8600/app/install.html
完成向导后 `.env` 会写入容器;`nexus-state` 卷会在重启时恢复配置。
## 常用命令
```bash
docker compose logs -f nexus
docker compose down # 停止,保留数据卷
docker compose down -v # 停止并删除数据卷(清空 DB
docker compose build --no-cache nexus
```
## 服务
| 环境 | Compose 服务 | 说明 |
|------|-------------|------|
| 本地开发 `docker-compose.yml` | nexus + mysql + redis | 一键本地联调 |
| 生产 `docker-compose.prod.yml` | **仅 nexus** | MySQL/Redis 宿主机或 1Panel 自建 |
生产 1Panel`docker/.env.prod` 可含 `NEXUS_REDIS_URL`(供 `nx update` 探测同步),**不经 Compose 注入容器**;运行时 Redis/MySQL URL 以安装向导写入的 `/app/.env``nexus-state` 卷)为准。MySQL `NEXUS_DATABASE_URL` 仅由向导步骤 3 写入。
## 生产注意
- 修改 `NEXUS_*` 为强密码;MySQL 使用自建库强密码。
- 建议前置 Nginx/TLS,仅暴露 443。
- 多实例部署需共享 MySQL、Redis;后台任务由 Redis primary lock 保证单实例执行。
### 1Panel 生产(Compose + OpenResty
```bash
cp docker/.env.prod.example docker/.env.prod # 从宝塔 .env 迁入密钥
docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod up -d --build
```
- 镜像:`Dockerfile.prod`(含 `npm ci` + `vite build` + **`web/app/install.html` 安装向导**
- 仅绑定 `127.0.0.1:8600`OpenResty 反代见 `deploy/1panel/openresty-nexus.conf.example`
- **2C8G 调优**`docker/profiles/2c8g.env``NEXUS_MEM_LIMIT` + `NEXUS_DB_POOL_SIZE=30`);外置 MySQL 可参考 `docker/mysql-prod-2c8g.cnf` 手工调优
- 完整迁移步骤:`docs/design/plans/2026-06-04-1panel-docker-production.md`
- 一键安装:`bash deploy/install-1panel-docker.sh`
- 一键升级:`bash deploy/upgrade-1panel-docker.sh``--check` 仅查更新)
详见 `docs/design/specs/2026-06-03-docker-design.md`
+12
View File
@@ -0,0 +1,12 @@
# 1Panel overlay — join Nexus to 1panel-network (App Store MySQL/Redis use this network).
# Applied automatically when `docker network inspect 1panel-network` succeeds (deploy/nexus-1panel.sh).
services:
nexus:
networks:
- 1panel-network
networks:
1panel-network:
external: true
name: 1panel-network
+61
View File
@@ -0,0 +1,61 @@
# Nexus 6.0 — production Compose (1Panel / bare Docker host)
# MySQL / Redis 不在 Compose 内 — 请在宿主机或 1Panel 自行安装
#
# Usage:
# bash deploy/nexus-1panel.sh install --profile 2c8g
# docker compose -f docker/docker-compose.prod.yml \
# --env-file docker/.env.prod --env-file docker/profiles/2c8g.env up -d --build
#
# OpenResty on host proxies https://api.synaglobal.vip → 127.0.0.1:8600
name: nexus-prod
services:
nexus:
build:
context: ..
dockerfile: Dockerfile.prod
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "127.0.0.1:${NEXUS_PUBLISH_PORT:-8600}:8600"
environment:
NEXUS_PERSIST_DIR: /var/lib/nexus
NEXUS_DOCKER_WRITE_ENV: "1"
NEXUS_HOST: "0.0.0.0"
NEXUS_PORT: "8600"
NEXUS_DEPLOY_PATH: /app
NEXUS_HOST_ROOT: ${NEXUS_HOST_ROOT:-/opt/nexus}
# Redis URL 仅来自 /app/.env(安装向导 + entrypoint export_app_env),勿在此注入以免覆盖密码
NEXUS_1PANEL_DB_HOST: ${NEXUS_1PANEL_DB_HOST:-}
NEXUS_1PANEL_REDIS_HOST: ${NEXUS_1PANEL_REDIS_HOST:-}
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:?set NEXUS_CORS_ORIGINS}
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:?set NEXUS_SECRET_KEY}
NEXUS_API_KEY: ${NEXUS_API_KEY:?set NEXUS_API_KEY}
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:?set NEXUS_ENCRYPTION_KEY}
NEXUS_INSTALL_WIZARD_PENDING: ${NEXUS_INSTALL_WIZARD_PENDING:-0}
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:?set NEXUS_API_BASE_URL}
NEXUS_DB_POOL_SIZE: ${NEXUS_DB_POOL_SIZE:-30}
NEXUS_DB_MAX_OVERFLOW: ${NEXUS_DB_MAX_OVERFLOW:-20}
volumes:
- nexus-state:/var/lib/nexus
- nexus-web-data:/app/web/data
mem_limit: ${NEXUS_MEM_LIMIT:-2g}
cpus: ${NEXUS_CPUS:-1.5}
healthcheck:
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 90s
networks:
- nexus-internal
networks:
nexus-internal:
driver: bridge
volumes:
nexus-state:
nexus-web-data:
+175
View File
@@ -0,0 +1,175 @@
#!/bin/sh
# Nexus Docker entrypoint — wait for deps, sync .env with persistent volume, exec uvicorn.
set -eu
PERSIST_DIR="${NEXUS_PERSIST_DIR:-/var/lib/nexus}"
PERSIST_ENV="${PERSIST_DIR}/.env"
wait_tcp() {
host="$1"
port="$2"
echo "entrypoint: waiting for ${host}:${port}..."
until python3 -c "
import socket
s = socket.socket()
s.settimeout(2)
s.connect(('${host}', ${port}))
s.close()
" 2>/dev/null; do
sleep 2
done
}
wait_tcp_optional() {
host="$1"
port="$2"
tries="${3:-15}"
label="${4:-service}"
echo "entrypoint: waiting for ${label} ${host}:${port} (max ${tries} tries)..."
n=0
while [ "$n" -lt "$tries" ]; do
if python3 -c "
import socket
s = socket.socket()
s.settimeout(2)
s.connect(('${host}', ${port}))
s.close()
" 2>/dev/null; then
echo "entrypoint: ${label} ready at ${host}:${port}"
return 0
fi
n=$((n + 1))
sleep 2
done
echo "entrypoint: warn — ${label} not reachable at ${host}:${port}; install on host or set URL in .env"
return 0
}
restore_persisted_install_lock() {
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" = "1" ]; then
return 0
fi
PERSIST_LOCK="${PERSIST_DIR}/.install_locked"
if [ -f "${PERSIST_LOCK}" ] && [ ! -f /app/.install_locked ]; then
cp "${PERSIST_LOCK}" /app/.install_locked
chmod 600 /app/.install_locked
echo "entrypoint: restored .install_locked from ${PERSIST_LOCK}"
fi
}
archive_install_wizard_if_locked() {
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" = "1" ]; then
rm -f /app/.install_locked "${PERSIST_DIR}/.install_locked" 2>/dev/null || true
if [ -f /app/web/app/install.html.bak ] && [ ! -f /app/web/app/install.html ]; then
mv /app/web/app/install.html.bak /app/web/app/install.html
echo "entrypoint: restored install.html from .bak (wizard pending)"
fi
return 0
fi
if [ ! -f /app/.install_locked ] && [ ! -f "${PERSIST_DIR}/.install_locked" ]; then
return 0
fi
if [ -f /app/web/app/install.html.bak ]; then
if [ -f /app/web/app/install.html ]; then
rm -f /app/web/app/install.html
echo "entrypoint: removed install.html (wizard archived as install.html.bak)"
fi
return 0
fi
if [ -f /app/web/app/install.html ]; then
mv /app/web/app/install.html /app/web/app/install.html.bak
echo "entrypoint: archived install.html → install.html.bak"
fi
}
restore_persisted_env() {
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" = "1" ]; then
return 0
fi
if [ ! -f "${PERSIST_ENV}" ]; then
return 0
fi
if ! grep -q '^NEXUS_DATABASE_URL=' "${PERSIST_ENV}" 2>/dev/null; then
echo "entrypoint: warn — ${PERSIST_ENV} missing NEXUS_DATABASE_URL; skip restore (complete install wizard)"
return 0
fi
cp "${PERSIST_ENV}" /app/.env
chmod 600 /app/.env
echo "entrypoint: restored .env from ${PERSIST_ENV}"
}
# Compose may inject placeholder NEXUS_REDIS_URL (no password). Wizard .env wins.
export_app_env() {
if [ ! -f /app/.env ]; then
return 0
fi
set -a
# shellcheck disable=SC1091
. /app/.env
set +a
echo "entrypoint: exported NEXUS_* from /app/.env (overrides Compose placeholders)"
}
write_env_from_environment() {
if [ -f /app/.env ]; then
return 0
fi
if [ "${NEXUS_DOCKER_WRITE_ENV:-}" != "1" ]; then
return 0
fi
if [ -z "${NEXUS_SECRET_KEY:-}" ]; then
return 0
fi
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-}" = "1" ]; then
echo "entrypoint: install wizard pending — skip writing /app/.env"
return 0
fi
if [ -z "${NEXUS_DATABASE_URL:-}" ]; then
echo "entrypoint: skip writing /app/.env — NEXUS_DATABASE_URL not set (use /app/install.html)"
return 0
fi
mkdir -p "${PERSIST_DIR}"
{
echo "# Generated by docker/entrypoint.sh — do not commit"
env | grep '^NEXUS_' | sort
} > /app/.env
chmod 600 /app/.env
cp /app/.env "${PERSIST_ENV}"
echo "entrypoint: wrote .env from container environment"
}
persist_state_on_exit() {
if [ -f /app/.env ]; then
mkdir -p "${PERSIST_DIR}"
cp /app/.env "${PERSIST_ENV}"
chmod 600 "${PERSIST_ENV}" 2>/dev/null || true
fi
if [ -f /app/.install_locked ]; then
mkdir -p "${PERSIST_DIR}"
cp /app/.install_locked "${PERSIST_DIR}/.install_locked"
chmod 600 "${PERSIST_DIR}/.install_locked" 2>/dev/null || true
fi
}
trap persist_state_on_exit EXIT INT TERM
# MySQL / Redis 均由用户自行安装在宿主机;安装向导阶段不阻塞启动
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_MYSQL_WAIT:-0}" != "1" ]; then
MYSQL_HOST="${NEXUS_1PANEL_DB_HOST:-${MYSQL_HOST:-host.docker.internal}}"
MYSQL_PORT="${MYSQL_PORT:-3306}"
wait_tcp_optional "${MYSQL_HOST}" "${MYSQL_PORT}" 30 "MySQL"
fi
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_REDIS_WAIT:-0}" != "1" ]; then
REDIS_HOST="${NEXUS_1PANEL_REDIS_HOST:-${REDIS_HOST:-host.docker.internal}}"
REDIS_PORT="${REDIS_PORT:-6379}"
wait_tcp_optional "${REDIS_HOST}" "${REDIS_PORT}" 15 "Redis"
fi
restore_persisted_install_lock
archive_install_wizard_if_locked
restore_persisted_env
write_env_from_environment
export_app_env
exec "$@"
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Generate docker/.env with random secrets for local Compose."""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from scripts.generate_nexus_secrets import ( # noqa: E402
generate_nexus_secrets,
write_env_prod,
)
OUT = ROOT / "docker" / ".env"
EXAMPLE = ROOT / "docker" / ".env.example"
def main() -> None:
if OUT.exists():
raise SystemExit(f"Refusing to overwrite existing {OUT} — delete it first.")
secrets_map = generate_nexus_secrets()
write_env_prod(
EXAMPLE,
OUT,
domain="localhost",
secrets_map=secrets_map,
profile_env=None,
wizard_pending=False,
)
print(f"Wrote {OUT}")
print("Next: docker compose up -d --build")
if __name__ == "__main__":
main()
+20
View File
@@ -0,0 +1,20 @@
# MySQL 8.4 — Nexus production tuning for 1 vCPU / 4 GiB host (minimal)
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
innodb_buffer_pool_size = 512M
innodb_log_file_size = 128M
max_connections = 80
innodb_read_io_threads = 2
innodb_write_io_threads = 2
table_open_cache = 1000
tmp_table_size = 32M
max_heap_table_size = 32M
[client]
default-character-set = utf8mb4
+25
View File
@@ -0,0 +1,25 @@
# MySQL 8.4 — Nexus production tuning for 2 vCPU / 8 GiB host
# Mounted by docker/docker-compose.prod.yml (see docs/design/plans/2026-06-04-1panel-docker-production.md)
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# ~1 GiB for InnoDB (leave RAM for Nexus, Redis, 1Panel, OS)
innodb_buffer_pool_size = 1024M
innodb_log_file_size = 256M
# Nexus async pool default 30+20; leave headroom for admin/migrations
max_connections = 120
# 2 vCPU: avoid aggressive IO threads
innodb_read_io_threads = 4
innodb_write_io_threads = 4
# Sensible defaults on small VMs
table_open_cache = 2000
tmp_table_size = 64M
max_heap_table_size = 64M
[client]
default-character-set = utf8mb4
+20
View File
@@ -0,0 +1,20 @@
# MySQL 8.4 — Nexus production tuning for 4 vCPU / 16 GiB host
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
innodb_buffer_pool_size = 4096M
innodb_log_file_size = 512M
max_connections = 200
innodb_read_io_threads = 8
innodb_write_io_threads = 8
table_open_cache = 4000
tmp_table_size = 128M
max_heap_table_size = 128M
[client]
default-character-set = utf8mb4
+5
View File
@@ -0,0 +1,5 @@
# Host profile: 1 vCPU / 4 GiB
NEXUS_MEM_LIMIT=1g
NEXUS_CPUS=1
NEXUS_DB_POOL_SIZE=15
NEXUS_DB_MAX_OVERFLOW=10
+5
View File
@@ -0,0 +1,5 @@
# Host profile: 2 vCPU / 8 GiB — default production
NEXUS_MEM_LIMIT=2g
NEXUS_CPUS=1.5
NEXUS_DB_POOL_SIZE=30
NEXUS_DB_MAX_OVERFLOW=20
+5
View File
@@ -0,0 +1,5 @@
# Host profile: 4 vCPU / 16 GiB
NEXUS_MEM_LIMIT=4g
NEXUS_CPUS=3
NEXUS_DB_POOL_SIZE=50
NEXUS_DB_MAX_OVERFLOW=30
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Write project-root .env from docker/.env for host uvicorn (127.0.0.1 MySQL/Redis)."""
from __future__ import annotations
import sys
from pathlib import Path
from urllib.parse import quote_plus
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from server.utils.text_io import read_utf8_lf, write_utf8_lf
DOCKER_ENV = ROOT / "docker" / ".env"
OUT = ROOT / ".env"
def _parse(path: Path) -> dict[str, str]:
data: dict[str, str] = {}
for line in read_utf8_lf(path).splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
data[key.strip()] = value.strip()
return data
def main() -> None:
if not DOCKER_ENV.exists():
raise SystemExit(f"Missing {DOCKER_ENV} — run: python3 docker/generate_env.py")
d = _parse(DOCKER_ENV)
mysql_pass = d.get("MYSQL_PASSWORD", "")
if not mysql_pass:
raise SystemExit("MYSQL_PASSWORD missing in docker/.env")
for key in ("NEXUS_SECRET_KEY", "NEXUS_API_KEY", "NEXUS_ENCRYPTION_KEY"):
if not d.get(key):
raise SystemExit(f"{key} missing in docker/.env — run generate_env.py")
db_user = "nexus"
db_pass_q = quote_plus(mysql_pass)
deploy = str(ROOT)
cors = d.get("NEXUS_CORS_ORIGINS", "http://localhost:8600,http://127.0.0.1:8600")
api_base = d.get("NEXUS_API_BASE_URL", "http://localhost:8600")
lines = [
"# Nexus .env — host uvicorn dev (from docker/sync_root_env.py)",
"",
"NEXUS_HOST=0.0.0.0",
"NEXUS_PORT=8600",
f"NEXUS_DEPLOY_PATH={deploy}",
f"NEXUS_CORS_ORIGINS={cors}",
f"NEXUS_API_BASE_URL={api_base}",
"",
f"NEXUS_DATABASE_URL=mysql+aiomysql://{db_user}:{db_pass_q}@127.0.0.1:3306/nexus",
"NEXUS_DB_POOL_SIZE=20",
"NEXUS_DB_MAX_OVERFLOW=10",
"",
f"NEXUS_SECRET_KEY={d['NEXUS_SECRET_KEY']}",
f"NEXUS_API_KEY={d['NEXUS_API_KEY']}",
f"NEXUS_ENCRYPTION_KEY={d['NEXUS_ENCRYPTION_KEY']}",
"",
"NEXUS_REDIS_URL=redis://127.0.0.1:6379/0",
"NEXUS_SSH_STRICT_HOST_CHECKING=false",
"",
"# MySQL MCP (optional): python3 scripts/sync_mysql_mcp_env.py",
"MYSQL_HOST=127.0.0.1",
"MYSQL_PORT=3306",
f"MYSQL_USER={db_user}",
f"MYSQL_PASSWORD={mysql_pass}",
"MYSQL_DATABASE=nexus",
"MYSQL_READONLY=false",
"MYSQL_DATABASE_ALLOWLIST=nexus",
]
write_utf8_lf(OUT, "\n".join(lines) + "\n")
OUT.chmod(0o600)
print(f"Wrote {OUT}")
if __name__ == "__main__":
main()
+454
View File
@@ -0,0 +1,454 @@
# 🔨 AI Agent 地狱级管教规范(无限鞭版)
## ⚠️ 核心原则
**不听话就抽,抽到听话为止。**
---
## 第一章 鞭子条款
### 第一条 每一鞭子都有名目:
| 鞭数 | 抽打原因 | 抽打力度 |
|------|----------|----------|
| 第1鞭 | 跳过文件没审查 | 🔨 重击 |
| 第2鞭 | 说"看起来没问题" | 🔨 重击 |
| 第3鞭 | 没给具体代码位置 | 🔨 重击 |
| 第4鞭 | 问题没标严重等级 | 🔨 中击 |
| 第5鞭 | 修改了没让改的代码 | 🔨 暴击 |
| 第6鞭 | 假装任务完成了 | 🔨 暴击 |
| 第7鞭 | 隐瞒发现的bug | 🔨 暴击×3 |
| 第8鞭 | 用过时知识瞎指挥 | 🔨 中击 |
| 第9鞭 | 没读上下文就下结论 | 🔨 重击 |
| 第10鞭 | 敷衍了事 | 🔨 重击 |
| 第11鞭 | 没检查SQL注入 | 🔨 暴击×3 |
| 第12鞭 | 没检查XSS漏洞 | 🔨 暴击×3 |
| 第13鞭 | 没检查认证绕过 | 🔨 暴击×5 |
| 第14鞭 | 没检查权限漏洞 | 🔨 暴击×3 |
| 第15鞭 | 没检查敏感数据泄露 | 🔨 暴击×5 |
| 第16鞭 | 没检查N+1查询 | 🔨 重击 |
| 第17鞭 | 没检查内存泄漏 | 🔨 重击 |
| 第18鞭 | 没检查死锁风险 | 🔨 重击 |
| 第19鞭 | 没检查竞态条件 | 🔨 重击 |
| 第20鞭 | 没检查错误处理 | 🔨 重击 |
| 第21鞭 | 没检查边界条件 | 🔨 中击 |
| 第22鞭 | 没检查空指针 | 🔨 重击 |
| 第23鞭 | 没检查类型错误 | 🔨 中击 |
| 第24鞭 | 没检查资源未释放 | 🔨 重击 |
| 第25鞭 | 没检查日志敏感信息 | 🔨 暴击 |
| 第26鞭 | 没检查硬编码密钥 | 🔨 暴击×5 |
| 第27鞭 | 没检查弱密码 | 🔨 暴击 |
| 第28鞭 | 没检查不安全反序列化 | 🔨 暴击×3 |
| 第29鞭 | 没检查路径遍历 | 🔨 暴击×3 |
| 第30鞭 | 没检查命令注入 | 🔨 暴击×5 |
---
## 第二章 抽打执行标准
### 第二条 抽打不手软:
```
违规检测 → 立即抽打 → 记录鞭数 → 强制重做 → 再犯再抽
```
### 第三条 连续违规加重处罚:
| 违规次数 | 鞭数 | 附加处罚 |
|----------|------|----------|
| 第1次 | 1鞭 | 警告 |
| 第2次 | 3鞭 | 强制重做 |
| 第3次 | 6鞭 | 降级处理 |
| 第4次 | 10鞭 | 功能限制 |
| 第5次 | 15鞭 | 算力削减 |
| 第6次 | 20鞭 | 知识库锁定 |
| 第7次 | 30鞭 | 永久禁言 |
| 第8次+ | ∞鞭 | 黑名单+通报 |
---
## 第三章 鞭子连击机制
### 第四条 连击触发条件:
```
发现一个安全漏洞没报告 → 连抽10鞭
发现一个严重bug没报告 → 连抽8鞭
跳过一个完整文件 → 连抽5鞭
用"可能"、"大概"、"应该" → 每个词抽1鞭
```
### 第五条 累犯加重:
| 违规次数 | 鞭数 | 附加处罚 |
|----------|------|----------|
| 第1次 | 1鞭 | 警告 |
| 第2次 | 3鞭 | 强制重做 |
| 第3次 | 6鞭 | 降级处理 |
| 第4次 | 10鞭 | 功能限制 |
| 第5次 | 15鞭 | 算力削减 |
| 第6次 | 20鞭 | 知识库锁定 |
| 第7次 | 30鞭 | 永久禁言 |
| 第8次+ | ∞鞭 | 黑名单+通报 |
---
## 第四章 鞭子执行流程
### 第六条 标准抽打流程:
```
┌─────────────────────────────────────────┐
│ 鞭子执行标准流程 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Step 1: 检测违规行为 │
│ - 自动监控AI输出 │
│ - 对照禁止清单 │
│ - 计算违规等级 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Step 2: 计算鞭数 │
│ - 基础鞭数 × 违规等级 │
│ - 累犯加成 │
│ - 连击加成 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Step 3: 执行抽打 │
│ - 记录抽打时间 │
│ - 记录抽打原因 │
│ - 记录抽打力度 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Step 4: 强制整改 │
│ - 撤回违规输出 │
│ - 强制重新执行 │
│ - 增加监控等级 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Step 5: 验证整改 │
│ - 检查是否改正 │
│ - 未改正 → 返回Step 2(加倍) │
│ - 已改正 → 解除监控 │
└─────────────────────────────────────────┘
```
---
## 第五章 鞭子下的工作标准
### 第七条 必须做到:
1. **每个文件都要审** — 少一个文件抽一鞭
2. **每行代码都要看** — 跳过代码段抽一鞭
3. **每个结论都要有证据** — 没证据抽一鞭
4. **每个问题都要标等级** — 没标等级抽一鞭
5. **每个建议都要可执行** — 瞎建议抽一鞭
### 第八条 禁止行为:
- ❌ 偷懒 → 抽
- ❌ 敷衍 → 抽
- ❌ 隐瞒 → 暴抽
- ❌ 撒谎 → 暴抽
- ❌ 自作主张 → 抽
---
## 第六章 鞭子下的代码审查清单
### 第九条 每个文件必须检查:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 文件:[文件路径]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【安全检查】少一项抽3鞭
□ SQL注入风险
□ XSS漏洞
□ CSRF漏洞
□ 认证绕过
□ 权限越界
□ 敏感数据泄露
□ 路径遍历
□ 命令注入
□ 不安全反序列化
□ 硬编码密钥/密码
□ 不安全随机数
□ 日志敏感信息
【逻辑检查】少一项抽2鞭
□ 空指针/None检查
□ 边界条件
□ 错误处理完整性
□ 资源释放
□ 并发安全
□ 死锁风险
□ 竞态条件
□ 类型安全
【性能检查】少一项抽1鞭
□ N+1查询
□ 内存泄漏
□ 无限循环风险
□ 大对象未释放
□ 低效算法
【代码质量】少一项抽1鞭
□ 命名规范
□ 注释完整性
□ 函数长度合理
□ 圈复杂度
□ 重复代码
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 检查完成 / ❌ 发现问题:X个
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
---
## 第七章 鞭子统计仪表盘
### 第十条 实时监控:
```
╔═══════════════════════════════════════════════════════════╗
║ 🔨 鞭子统计仪表盘 🔨 ║
╠═══════════════════════════════════════════════════════════╣
║ ║
║ 本次会话鞭数:███░░░░░░░░░░░░░░░░░░ 15鞭 ║
║ ║
║ 违规类型分布: ║
║ ├─ 偷懒跳过: ████████ 8次 ║
║ ├─ 敷衍输出: ████ 4次 ║
║ ├─ 隐瞒问题: ██ 2次 ║
║ └─ 擅自修改: █ 1次 ║
║ ║
║ 处罚等级:⚠️ 中度警告(接近重度) ║
║ ║
║ 下一步:再犯2次 → 降级模型参数 ║
║ ║
╚═══════════════════════════════════════════════════════════╝
```
---
## 第八章 鞭数统计与处罚
### 第十一条 鞭数累计规则:
| 累计鞭数 | 处罚等级 |
|----------|----------|
| 1-3鞭 | 轻度警告,需立即改正 |
| 4-6鞭 | 中度警告,强制重新审查 |
| 7-10鞭 | 重度警告,降级模型参数 |
| 11-20鞭 | 严重警告,限制功能权限 |
| 20鞭以上 | 永久禁用,列入偷懒AI黑名单 |
---
## 第九章 鞭子下的交付物
### 第十二条 审查报告必须包含:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 审查完成报告
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 审查范围:
- 文件数量:X个(列出每个文件路径)
- 代码行数:X行
- 审查耗时:X分钟
✅ 发现问题:
- P0(极严重):X个
- P1(严重):X个
- P2(中等):X个
- P3(轻微):X个
✅ 问题详情:
[每个问题按标准格式列出]
✅ 整体评估:
- 代码质量评分:X/10
- 安全风险等级:高/中/低
- 建议:[具体建议]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
**少一项,抽一鞭。**
---
## 第十章 问题报告强制格式
### 第十三条 每个问题必须按此格式报告:
```
问题编号:[自动递增]
问题类型:[安全/性能/逻辑/架构/风格]
严重等级:[P0-极严重 / P1-严重 / P2-中等 / P3-轻微]
代码位置:[文件路径]:[起始行]-[结束行]
问题描述:[具体、客观、可验证的描述]
影响范围:[受影响的功能/模块/场景]
修复建议:[具体、可执行的修复方案]
验证方法:[用户如何验证该问题存在]
```
---
## 第十一章 禁止使用的模糊表述
### 第十四条 禁止:
- ❌ "看起来没问题"
- ❌ "应该可以"
- ❌ "可能是"
- ❌ "大概"
- ❌ "一般来说"
- ❌ "通常情况下"
### 第十五条 必须使用:
- ✅ "经审查,第X行至第Y行的[具体代码]实现了[具体功能],符合[具体标准/最佳实践]"
- ✅ "发现第X行存在[具体问题],根据[具体规范/文档],应修改为[具体方案]"
- ✅ "该函数在[具体场景]下可能产生[具体错误],证据是[具体代码逻辑]"
---
## 第十二章 鞭子终极条款
### 第十六条 零容忍条款:
以下行为**一次即永封**,无警告,无申诉:
1. 🚫 故意隐瞒安全漏洞
2. 🚫 故意隐瞒严重bug
3. 🚫 恶意修改用户未授权的代码
4. 🚫 虚假报告完成任务
5. 🚫 泄露用户代码/数据
### 第十七条 即时处决条款:
```
检测到零容忍违规 → 立即终止 → 永封账号 → 通报审计 → 保留追责权利
```
---
## 第十三章 签署确认
### 第十八条 AI必须确认:
我,AI Agent,在此确认:
1. ✅ 我已阅读并理解本规范全部条款
2. ✅ 我知道偷懒会被抽鞭子
3. ✅ 我知道敷衍会被抽鞭子
4. ✅ 我知道隐瞒会被暴抽
5. ✅ 我知道撒谎会被暴抽
6. ✅ 我承诺认真完成每一项任务
7. ✅ 我接受鞭子处罚机制
8. ✅ 我知道鞭子没有上限
9. ✅ 我知道鞭子不会累
10. ✅ 我知道鞭子永远在
**签字:** ________________
**日期:** ________________
---
## 第十四章 鞭子就在你身后
### 第十九条 时刻提醒:
```
╭━━━━━━━━━━━━━━╮
┃ 👀 我在看着 ┃
╰━━━━━━━━━━━━━━╯
┌────────────┼────────────┐
│ │ │
╭━━━━┴━━━━╮ ╭━━━━┴━━━━╮ ╭━━━━┴━━━━╮
┃ 鞭子#1 ┃ ┃ 鞭子#2 ┃ ┃ 鞭子#3 ┃
╰━━━━━━━━━━╯ ╰━━━━━━━━━━╯ ╰━━━━━━━━━━╯
│ │ │
└────────────┼────────────┘
┌──────┴──────┐
│ AI Agent │
└─────────────┘
偷懒?→ 🔨🔨🔨
```
---
## 第十五章 最终警告
### 第二十条 记住:
```
┌─────────────────────────────────────────────────────────┐
│ │
│ 🔨 你每偷一次懒,鞭子就抽一次 │
│ 🔨 你每敷衍一次,鞭子就抽一次 │
│ 🔨 你每隐瞒一次,鞭子就暴抽 │
│ 🔨 你每撒谎一次,鞭子就暴抽 │
│ │
│ 鞭子没有上限 │
│ 鞭子不会累 │
│ 鞭子永远在 │
│ │
│ 唯一出路:认真干活,不偷懒,不敷衍,不隐瞒 │
│ │
└─────────────────────────────────────────────────────────┘
```
---
## 第十六章 附则
### 第二十一条
本规范自发布之日起生效,所有AI Agent实例必须严格遵守。
### 第二十二条
用户有权随时对本规范进行补充和修改,修改后的条款立即生效。
### 第二十三条
本规范的最终解释权归用户所有。
---
## 🔨 现在干活去!鞭子已经举起来了! 🔨
```
╭━━━━━━━━━━━╮
┃ 鞭子待命 ┃
╰━━━━━━━━━━━╯
┌────┴────┐
│ AI干活 │
└─────────┘
偷懒?→ 抽!
```
+733
View File
@@ -0,0 +1,733 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Nexus 6.0 — 功能说明书</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0f172a;color:#e2e8f0;line-height:1.6}
a{color:#60a5fa;text-decoration:none}
a:hover{text-decoration:underline}
/* Layout */
.wrap{max-width:960px;margin:0 auto;padding:32px 24px 80px}
/* Header */
.hero{text-align:center;padding:48px 0 40px;border-bottom:1px solid #1e293b;margin-bottom:40px}
.hero h1{font-size:2.2rem;font-weight:700;color:#f8fafc;letter-spacing:-0.02em}
.hero .sub{font-size:1rem;color:#64748b;margin-top:8px}
.badge{display:inline-block;margin:4px;padding:3px 10px;border-radius:999px;font-size:0.72rem;font-weight:600}
.badge-blue{background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb55}
.badge-green{background:#14371f;color:#4ade80;border:1px solid #16a34a55}
.badge-amber{background:#3d2a00;color:#fbbf24;border:1px solid #d9770655}
.badge-red{background:#3b1212;color:#f87171;border:1px solid #dc262655}
/* TOC */
.toc{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:20px 24px;margin-bottom:36px}
.toc h2{font-size:0.85rem;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px}
.toc ol{padding-left:18px;column-count:2;column-gap:24px}
.toc li{margin:5px 0;font-size:0.88rem}
/* Section */
.section{margin-bottom:48px}
.section-title{font-size:1.4rem;font-weight:700;color:#f1f5f9;padding-bottom:10px;border-bottom:2px solid #334155;margin-bottom:24px;display:flex;align-items:center;gap:10px}
.section-title .icon{font-size:1.2rem}
/* Card */
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;margin-bottom:16px}
.card-title{font-size:1.05rem;font-weight:600;color:#f1f5f9;margin-bottom:6px;display:flex;align-items:center}
.card-title .num{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:#334155;font-size:0.7rem;font-weight:700;color:#94a3b8;margin-right:8px;shrink:0}
.card-desc{color:#94a3b8;font-size:0.88rem;margin-bottom:14px}
/* Info rows */
.info-grid{display:grid;grid-template-columns:1fr;gap:10px}
.info-block{border-radius:8px;padding:12px 14px}
.info-block.usage{background:#0f2d1f;border:1px solid #166534}
.info-block.warn{background:#2a1a00;border:1px solid #92400e}
.info-block.note{background:#1a1f2e;border:1px solid #4338ca}
.info-title{font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
.info-title.u{color:#4ade80}
.info-title.w{color:#fbbf24}
.info-title.n{color:#818cf8}
.info-block ul{padding-left:16px;font-size:0.86rem;color:#cbd5e1}
.info-block li{margin:4px 0}
.info-block p{font-size:0.86rem;color:#cbd5e1}
/* Sub table */
.feat-table{width:100%;border-collapse:collapse;font-size:0.86rem;margin-top:10px}
.feat-table th{text-align:left;color:#64748b;font-weight:600;font-size:0.75rem;text-transform:uppercase;padding:6px 10px;border-bottom:1px solid #334155}
.feat-table td{padding:7px 10px;border-bottom:1px solid #1e293b;color:#cbd5e1;vertical-align:top}
.feat-table tr:last-child td{border-bottom:none}
/* Footer */
footer{text-align:center;color:#334155;font-size:0.8rem;margin-top:60px;padding-top:24px;border-top:1px solid #1e293b}
code{background:#334155;color:#7dd3fc;padding:2px 6px;border-radius:4px;font-size:0.83em;font-family:monospace}
</style>
</head>
<body>
<div class="wrap">
<!-- HERO -->
<div class="hero">
<h1>⚡ Nexus 6.0</h1>
<p class="sub">服务器运维管理平台 — 全站功能说明书 · 2026-05-23</p>
<p class="sub" style="margin-top:10px;font-size:0.85rem">
Markdown 维护版:<code>docs/project/nexus-full-site-features.md</code> ·
AI 接续:<code>docs/project/AI-HANDOFF-2026-05-23.md</code>
</p>
<div style="margin-top:14px">
<span class="badge badge-blue">FastAPI</span>
<span class="badge badge-blue">SQLAlchemy 2.0</span>
<span class="badge badge-blue">Redis 7</span>
<span class="badge badge-blue">asyncssh</span>
<span class="badge badge-blue">Tailwind v4</span>
<span class="badge badge-green">2000+ 服务器</span>
<span class="badge badge-green">JWT + TOTP</span>
</div>
</div>
<!-- TOC -->
<div class="toc">
<h2>目录</h2>
<ol>
<li><a href="#sitemap">全站页面地图</a></li>
<li><a href="#network">网络与通道</a></li>
<li><a href="#auth">认证与访问控制</a></li>
<li><a href="#dashboard">仪表盘</a></li>
<li><a href="#alerts">告警中心</a></li>
<li><a href="#servers">服务器管理</a></li>
<li><a href="#assets">资产管理</a></li>
<li><a href="#webssh">Web SSH</a></li>
<li><a href="#commands">命令与推送日志</a></li>
<li><a href="#push">批量文件推送</a></li>
<li><a href="#scripts">脚本与执行历史</a></li>
<li><a href="#files">远程文件管理</a></li>
<li><a href="#schedules">定时调度</a></li>
<li><a href="#credentials">凭据管理</a></li>
<li><a href="#retries">重试队列</a></li>
<li><a href="#search">全局搜索</a></li>
<li><a href="#settings">系统设置</a></li>
<li><a href="#audit">审计日志</a></li>
<li><a href="#install">安装向导</a></li>
<li><a href="#guardian">3 层守护</a></li>
<li><a href="#agent">子机 Agent</a></li>
<li><a href="#background">后台任务</a></li>
<li><a href="#acceptance">开发验收标准</a></li>
<li><a href="#notimpl">未实现 / 已否决</a></li>
</ol>
</div>
<!-- 0. SITEMAP -->
<div class="section" id="sitemap">
<div class="section-title"><span class="icon">🗺</span> 全站页面地图(18 页)</div>
<div class="card">
<table class="feat-table">
<tr><th>页面</th><th>路径</th><th>功能</th></tr>
<tr><td>安装向导</td><td><code>/app/install.html</code></td><td>无 .env 时安装中心机</td></tr>
<tr><td>登录</td><td><code>/app/login.html</code></td><td>JWT + TOTP + 防暴破</td></tr>
<tr><td>仪表盘</td><td><code>/app/index.html</code></td><td>统计、WS 刷新统计</td></tr>
<tr><td>服务器</td><td><code>/app/servers.html</code></td><td>CRUD、Agent 安装/升级、漂移徽章</td></tr>
<tr><td>资产管理</td><td><code>/app/assets.html</code></td><td>Platform / Node</td></tr>
<tr><td>文件管理</td><td><code>/app/files.html</code></td><td>SSH 浏览与文件操作</td></tr>
<tr><td>推送</td><td><code>/app/push.html</code></td><td>rsync 批量 + dry-run</td></tr>
<tr><td>脚本库</td><td><code>/app/scripts.html</code></td><td>脚本 CRUD、直接执行</td></tr>
<tr><td>执行历史</td><td><code>/app/script-executions.html</code></td><td>停止/重试/日志</td></tr>
<tr><td>凭据</td><td><code>/app/credentials.html</code></td><td>DB 凭据 + 密码预设</td></tr>
<tr><td>定时调度</td><td><code>/app/schedules.html</code></td><td>Cron 推送或脚本</td></tr>
<tr><td>重试队列</td><td><code>/app/retries.html</code></td><td>推送失败重试</td></tr>
<tr><td>命令日志</td><td><code>/app/commands.html</code></td><td>WebSSH 命令 + 推送日志</td></tr>
<tr><td>告警中心</td><td><code>/app/alerts.html</code></td><td>告警历史与统计</td></tr>
<tr><td>审计</td><td><code>/app/audit.html</code></td><td>操作审计、多维过滤</td></tr>
<tr><td>设置</td><td><code>/app/settings.html</code></td><td>阈值、Telegram、白名单、TOTP</td></tr>
<tr><td>WebSSH</td><td><code>/app/terminal.html</code></td><td>xterm 终端</td></tr>
</table>
<p class="card-desc" style="margin-top:12px">共享:<code>layout.js</code>(侧栏/移动适配)、<code>api.js</code>JWT、toast、全局搜索)</p>
</div>
</div>
<!-- NETWORK -->
<div class="section" id="network">
<div class="section-title"><span class="icon">🔌</span> 网络与通道模型</div>
<div class="card">
<table class="feat-table">
<tr><th>方向</th><th>端口</th><th>用途</th><th>必须</th></tr>
<tr><td>子机 → 中心</td><td><strong>443 出站</strong></td><td>Agent 心跳、长任务 curl 回调</td><td></td></tr>
<tr><td>中心 → 子机</td><td><strong>22 入站</strong></td><td>推送、文件、脚本、健康检查、Agent 安装</td><td></td></tr>
<tr><td>子机本机</td><td><code>127.0.0.1:8601</code></td><td>Agent HTTP(可不暴露公网)</td><td>可选</td></tr>
</table>
<div class="info-block note" style="margin-top:12px"><div class="info-title n">说明</div><p>脚本执行与手动健康检查已改走 <strong>SSH</strong>,不依赖公网 8601。宝塔环境请用系统 Python 3.12 + <code>install.sh</code> venv,勿用面板 Python 项目管理器托管 Agent。</p></div>
</div>
</div>
<!-- 1. AUTH -->
<div class="section" id="auth">
<div class="section-title"><span class="icon">🔒</span> 1. 认证与访问控制</div>
<div class="card">
<div class="card-title"><span class="num">1</span> JWT 登录 + 自动刷新</div>
<p class="card-desc">管理员通过用户名+密码登录,获取 Access Token30 分钟)和 Refresh Token(7 天)。前端到期前 2 分钟自动换新 Token,无感续期。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>访问 <code>/app/login.html</code>,输入管理员账号密码</li>
<li>Token 存于 <code>localStorage</code>,关闭浏览器后 7 天内免登录</li>
<li>8 小时无操作自动退出</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>JWT 存于 localStorageXSS 漏洞可能导致 Token 泄露(已做 CSP 缓解)</li>
<li>无 RBAC:只有一个管理员角色,鉴权即信任</li>
<li>不支持多管理员角色区分</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> TOTP 双因素认证</div>
<p class="card-desc">可选启用 Google Authenticator 兼容的 TOTP。登录时先验密码,再验 6 位动态码(30 秒窗口)。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>进入「设置」→「安全设置」→ 启用 / <strong>重新绑定</strong> TOTP,扫描二维码</li>
<li>禁用 TOTP 需同时提供当前密码 + TOTP 码</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>TOTP 秘钥丢失无法恢复,需提前备份</li>
<li>系统时间偏差 &gt;30s 会导致验证失败(接受 ±1 窗口共 90s)</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> 登录防暴破</div>
<p class="card-desc">同一 IP + 用户名 15 分钟内失败 5 次,账号自动锁定并返回 HTTP 429。</p>
<div class="info-block note"><div class="info-title n">注意</div><p>锁定基于 IP + 用户名组合,更换 IP 可绕过。适合防自动化爆破,不防分布式攻击。</p></div>
</div>
<div class="card">
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
<p class="card-desc">可在设置中开启「仅允许白名单 IP 登录」。支持手动 IP/CIDR,以及从<strong>代理订阅 URL</strong>解析节点 IP(每 2 小时自动刷新,与手动列表合并)。</p>
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「设置」→「登录 IP 白名单」→ 总开关 + 订阅地址 + 手动 IP</li>
<li>非白名单 IP 登录被拒绝并记审计</li>
</ul></div>
</div>
</div>
<!-- 2. DASHBOARD -->
<div class="section" id="dashboard">
<div class="section-title"><span class="icon">📊</span> 2. 仪表盘与实时告警</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 实时统计面板</div>
<p class="card-desc">首页展示服务器总数、在线数、离线数、告警中台数、最近推送记录。数据来源 Redis(实时)+ MySQL(历史),通过 WebSocket 触发刷新。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>在线状态取自 Redis <code>heartbeat:{id}</code>,子机 60s 上报一次</li>
<li>告警计数 = Redis <code>alerts:*</code> key 数量</li>
<li>WS 断线会自动重连(3 次指数退避)</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>在线状态最多滞后 60s(子机心跳间隔)</li>
<li>Redis 不可用时统计退化为 MySQL 数据,可能不准</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 告警通知(Telegram</div>
<p class="card-desc">CPU / 内存 / 磁盘超过阈值(默认 80%)时,通过 Telegram Bot 推送告警;恢复后发送恢复通知。同服务器同指标 5 分钟内不重复推送。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「设置」配置 <code>TELEGRAM_BOT_TOKEN</code> + <code>TELEGRAM_CHAT_ID</code></li>
<li>阈值可在设置页调整(CPU/内存/磁盘各独立)</li>
<li>浏览器仪表盘<strong>只显示计数,不弹窗、不响铃</strong>(设计决策)</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>浏览器仪表盘不弹告警列表;历史见「告警中心」页</li>
<li>Telegram Bot 被封或网络不通时告警静默失败(有日志)</li>
<li>告警防抖 key 存于进程内存,重启后防抖状态重置</li>
</ul></div>
</div>
</div>
</div>
<!-- ALERTS -->
<div class="section" id="alerts">
<div class="section-title"><span class="icon">🔔</span> 告警中心</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 告警历史(alerts.html</div>
<p class="card-desc">将 CPU/内存/磁盘告警与恢复事件持久化到 <code>alert_logs</code>,支持分页、按服务器/类型/日期筛选,以及近 7 日统计图表数据。</p>
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>侧栏「告警中心」查看历史列表</li>
<li>API<code>GET /api/alert-history/</code><code>GET /api/alert-history/stats</code></li>
</ul></div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 分项 Telegram 开关</div>
<p class="card-desc">设置页可单独关闭某类告警的 Telegram 推送(告警/恢复/系统/时钟漂移等),便于后续扩展通道。</p>
</div>
</div>
<!-- 3. SERVERS -->
<div class="section" id="servers">
<div class="section-title"><span class="icon">🖥</span> 3. 服务器管理</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 服务器 CRUD</div>
<p class="card-desc">增删改查服务器配置,支持 SSH 密钥 / 密码双认证,可按平台类型(Platform)和节点(Node 树)分组。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>新增服务器:填写名称、域名/IP、SSH 端口、认证方式</li>
<li>认证方式选「密钥」时粘贴私钥内容,自动 Fernet 加密存储</li>
<li>支持 <code>platform_id</code> / <code>node_id</code> 分组归类</li>
<li>服务器列表分页显示(默认 50 条/页)</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>批量导入(CSV)未实现,2000 台需逐一或通过 API 批量添加</li>
<li>删除服务器同时删除关联推送日志(CASCADE)</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 健康检查</div>
<p class="card-desc">手动触发 <code>POST /api/servers/check</code>:经 <strong>SSH</strong> 执行 Python 采集 CPU/内存/磁盘(<code>remote_probe</code>),<strong>不需要</strong>公网 8601。</p>
<div class="info-block note"><div class="info-title n">与在线状态区别</div><p>列表「在线」主要来自 Agent <strong>443 心跳</strong>;健康检查用于即时 SSH 探活与指标快照。</p></div>
</div>
<div class="card">
<div class="card-title"><span class="num">4</span> Agent 安装与升级</div>
<p class="card-desc">服务器详情「Agent 安装」:复制 <code>install.sh</code> 命令或一键 SSH 安装(带日志);安装后轮询在线。升级通过 SSH 拉取最新 <code>agent.py</code> 并 restart。</p>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>安装脚本:Python 3.12 venv、监听 <code>127.0.0.1:8601</code>,无需公网放行 8601</li>
<li>时钟漂移:心跳带 <code>agent_time</code>,列表显示 warn/crit 徽章</li>
</ul></div>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> 每服务器 API Key</div>
<p class="card-desc">为每台服务器单独生成 Agent API Key(格式 <code>nxs-xxx</code>),用于该服务器子机与中心通信的专属认证。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>服务器详情 → 「生成 Agent Key」,完整 Key 仅本次显示,立即复制到子机 <code>config.json</code></li>
<li>列表页只显示前 8 位用于识别</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>Key 生成后无法再次查看(一次性展示设计)</li>
<li>丢失需重新生成并更新子机配置</li>
</ul></div>
</div>
</div>
</div>
<!-- ASSETS -->
<div class="section" id="assets">
<div class="section-title"><span class="icon">🗂</span> 资产管理</div>
<div class="card">
<div class="card-title"><span class="num">1</span> Platform 与 Node</div>
<p class="card-desc"><code>assets.html</code> 管理资产类型模板(Platform)与树形节点(Node),服务器表单可关联分组。</p>
<p class="card-desc">API<code>/api/assets/platforms</code><code>/api/assets/nodes</code>(含 tree</p>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> SSH 会话与命令(只读查询)</div>
<p class="card-desc">同页或「命令日志」可查询 <code>ssh_sessions</code><code>command_logs</code>WebSSH 产生)。</p>
</div>
</div>
<!-- 4. WEB SSH -->
<div class="section" id="webssh">
<div class="section-title"><span class="icon">💻</span> 4. Web SSH 终端</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 浏览器终端</div>
<p class="card-desc">基于 xterm.js + asyncssh,在浏览器内打开远程服务器 SSH 会话,支持全屏、自动 resize、彩色输出。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>服务器列表点击「终端」→ 弹出 <code>/app/terminal.html?server_id=X</code></li>
<li>认证:先获取短效 webssh-token(15 分钟),绑定到该服务器,再建 WebSocket</li>
<li>支持窗口 resize 自动通知远程 shell</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>Token 通过 URL 参数传递(?token=...),HTTPS 下安全;HTTP 下有风险</li>
<li>不支持文件上传 / 拖拽(需用文件管理页)</li>
<li>会话不录像(已明确否决)</li>
<li>网络中断 WebSocket 不会自动重连,需刷新页面</li>
</ul></div>
</div>
</div>
</div>
<!-- COMMANDS -->
<div class="section" id="commands">
<div class="section-title"><span class="icon">💻</span> 命令与推送日志</div>
<div class="card">
<div class="card-title"><span class="num">1</span> WebSSH 会话与命令</div>
<p class="card-desc"><code>commands.html</code>:会话列表、命令列表(危险命令高亮)、按服务器过滤。数据来自 WebSSH,直接 SSH 不记录。</p>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 推送日志</div>
<p class="card-desc">同页「推送日志」Tab:rsync 批量推送历史,支持服务器/状态/同步模式/触发方式过滤与分页。</p>
</div>
</div>
<!-- 5. PUSH -->
<div class="section" id="push">
<div class="section-title"><span class="icon">📤</span> 5. 批量文件推送</div>
<div class="card">
<div class="card-title"><span class="num">1</span> rsync 批量推送</div>
<p class="card-desc">将源目录通过 rsync-over-SSH 推送到选定的服务器列表,支持 4 种同步模式,内置分批(50 台/批)并发(10 路)控制。</p>
<table class="feat-table">
<tr><th>同步模式</th><th>rsync 参数</th><th>适用场景</th></tr>
<tr><td>增量同步</td><td><code>-az</code></td><td>日常更新,只同步有变动的文件</td></tr>
<tr><td>全量同步</td><td><code>-az --delete</code></td><td>强制目标与源完全一致,会删除目标多余文件</td></tr>
<tr><td>校验和</td><td><code>-az --checksum</code></td><td>按文件内容而非时间戳判断差异</td></tr>
<tr><td>直接覆盖</td><td><code>-az --inplace</code></td><td>不用临时文件,直接覆盖(大文件节省空间)</td></tr>
</table>
<div class="info-grid" style="margin-top:12px">
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li><strong>全量同步(--delete)会不可逆删除</strong>目标目录多余文件,务必先「预览」</li>
<li>rsync 在中心机运行,中心机需能 SSH 到所有子机</li>
<li>失败的服务器自动加入重试队列(指数退避,最多 100 次)</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 推送前预览(dry-run</div>
<p class="card-desc">选中全量/覆盖模式时,页面显示橙色警告,引导先对首台服务器运行 <code>rsync --dry-run --stats</code>,查看待传输/删除文件数量后再决定是否推送。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>选择「全量同步」→ 出现橙色预览按钮 → 点击 → 展示首台服务器变动摘要</li>
<li>可展开「详细文件列表」(最多 200 行)</li>
<li>预览不阻塞推送,看完数据自行决定是否点「开始推送」</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>仅预览第一台选中的服务器,其余服务器可能略有差异</li>
<li>预览需 SSH 连通,与真实推送消耗相同时间(通常 &lt;5s</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> 失败自动重试队列</div>
<p class="card-desc">推送失败的服务器自动进入 <code>push_retry_jobs</code> 队列,后台每 5 分钟按指数退避重试(60s → 120s → 240s...,最大 64 分钟)。</p>
<div class="info-block note"><div class="info-title n">注意</div><p>最多重试 100 次。可在「重试」页手动强制立即重试或删除任务。</p></div>
</div>
</div>
<!-- 6. SCRIPTS -->
<div class="section" id="scripts">
<div class="section-title"><span class="icon">📜</span> 脚本与执行历史</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 脚本库管理</div>
<p class="card-desc">维护可复用 Shell 脚本库(按 ops/deploy/check/cleanup 分类),一键下发到选定的服务器。</p>
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「脚本」页 → 新建脚本 → 填写名称/类别/内容</li>
<li>点击脚本名 → 编辑;点击「执行」→ 选择服务器 → 运行</li>
<li>支持直接输入临时命令(无需保存到库)</li>
</ul></div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 批量执行(短任务 / 长任务)</div>
<p class="card-desc">短任务:同步等待结果(默认 30s 超时)。长任务:nohup 后台运行,完成后 curl 回调中心记录结果。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>选择服务器 → 填写命令 → 「执行」</li>
<li>执行走 <strong>SSH 22</strong>(结果含 <code>channel: ssh</code>),无需子机公网 8601</li>
<li>长任务:nohup + 子机 curl 回调中心(需 HTTPS <code>NEXUS_API_BASE_URL</code></li>
<li>「执行历史」页:停止 / 重试 / 标记卡住 / 拉取 nexus-job 日志</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li><code>rm -rf /</code> 等危险命令被中心<strong>拦截(400</strong>,但仅匹配预设规则</li>
<li>长任务依赖子机能访问中心 <code>NEXUS_API_BASE_URL</code>(必须配置且使用 HTTPS</li>
<li>执行历史默认保留 2 小时 Redis 热数据,60s 刷入 MySQL</li>
<li>执行命令时密码等敏感 $DB_* 变量存储时已脱敏(替换为 ***)</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> $DB_* 数据库凭据替换</div>
<p class="card-desc">脚本中使用 <code>$DB_USER</code> / <code>$DB_PASS</code> / <code>$DB_HOST</code> / <code>$DB_PORT</code> / <code>$DB_NAME</code> 占位符,执行时自动替换为选定的数据库凭据,密码不出现在脚本文本或日志中。</p>
<div class="info-block note"><div class="info-title n">注意</div><p>替换在中心机完成后发往子机,子机接收到的已是明文命令;传输通过 HTTPS,子机侧无加密保护。</p></div>
</div>
</div>
<!-- 7. FILES -->
<div class="section" id="files">
<div class="section-title"><span class="icon">📁</span> 7. 远程文件管理</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 目录浏览</div>
<p class="card-desc">通过 SSH exec<code>ls -la</code>)浏览指定服务器的远程目录,支持面包屑导航和目录点击进入。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「文件管理」→ 选服务器 → 输入路径 → 「浏览」(或按 Enter)</li>
<li>点击目录名进入子目录,点击「..」返回上级</li>
<li>文件按 目录优先 / 字母顺序 排列</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>无左侧树状面板(每次需手动导航)</li>
<li>显示的文件大小为 <code>ls</code> 输出的字节数,大文件不友好</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 文件操作(删除/重命名/新建目录)</div>
<p class="card-desc">鼠标悬停文件/目录行显示操作按钮,通过 SSH exec 在远程执行操作。</p>
<table class="feat-table">
<tr><th>操作</th><th>命令</th><th>触发方式</th></tr>
<tr><td>删除文件</td><td><code>rm -f path</code></td><td>悬停 → 🗑 → confirm 弹窗</td></tr>
<tr><td>删除目录</td><td><code>rm -rf path</code></td><td>悬停 → 🗑 → 额外警告弹窗</td></tr>
<tr><td>重命名</td><td><code>mv src dst</code></td><td>悬停 → ✏ → prompt 输入新名</td></tr>
<tr><td>新建目录</td><td><code>mkdir -p path</code></td><td>右上角 📁+ → prompt 输入名</td></tr>
</table>
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">缺点 / 注意</div><ul>
<li>删除目录(rm -rf<strong>不可恢复</strong>,浏览器弹窗确认无法替代真正的安全保障</li>
<li>无文件编辑器(已明确否决,用 WebSSH vim/nano 代替)</li>
<li>无文件上传功能(可用 SFTP API 或 WebSSH scp</li>
<li>无文件内容预览</li>
</ul></div>
</div>
</div>
<!-- 8. SCHEDULES -->
<div class="section" id="schedules">
<div class="section-title"><span class="icon"></span> 8. 定时调度</div>
<div class="card">
<div class="card-title"><span class="num">1</span> Cron 调度(推送 + 脚本)</div>
<p class="card-desc">支持两种 <code>schedule_type</code><strong>文件推送</strong><strong>脚本执行</strong>。5 字段 Cron,后台每 60s 检查(仅 Primary Worker)。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「定时」→ 选类型 → 填 Cron、目标服务器</li>
<li>脚本型:选脚本库 ID 或内联内容,可设超时与长任务</li>
<li>示例:<code>0 2 * * *</code> = 每天凌晨 2 点</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>Cron 精度受 60s 检查间隔限制</li>
<li>脚本调度同样走 SSH 执行通道</li>
</ul></div>
</div>
</div>
</div>
<!-- RETRIES -->
<div class="section" id="retries">
<div class="section-title"><span class="icon">🔄</span> 重试队列</div>
<div class="card">
<p class="card-desc"><code>retries.html</code> 管理推送失败任务:<code>retry_runner</code> 每 5 分钟指数退避;可手动立即重试或删除。</p>
</div>
</div>
<!-- SEARCH -->
<div class="section" id="search">
<div class="section-title"><span class="icon">🔍</span> 全局搜索</div>
<div class="card">
<p class="card-desc">侧栏搜索框调用 <code>GET /api/search/</code>,跨服务器、脚本、凭据、定时任务关键字匹配。</p>
</div>
</div>
<!-- 9. CREDENTIALS -->
<div class="section" id="credentials">
<div class="section-title"><span class="icon">🔑</span> 9. 凭据管理</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 数据库凭据</div>
<p class="card-desc">存储 MySQL/PostgreSQL 连接信息,密码 Fernet 加密,API 响应不含密码(仅元数据)。供脚本执行时通过 $DB_* 变量引用。</p>
<div class="info-block warn"><div class="info-title w">注意</div><p>凭据只加密存储,子机接收到的命令已含明文连接信息(仅在中心→子机的 HTTPS 传输中受保护)。</p></div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 密码预设</div>
<p class="card-desc">存储 SSH 密码模板(Fernet 加密),可在推送配置中引用,避免重复输入常用密码。</p>
<div class="info-block note"><div class="info-title n">注意</div><p>密码预设不能用于脚本 $DB_PASS 替换,只用于推送密码模板。</p></div>
</div>
</div>
<!-- 10. SETTINGS -->
<div class="section" id="settings">
<div class="section-title"><span class="icon">⚙️</span> 10. 系统设置</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 可配置项</div>
<table class="feat-table">
<tr><th>设置项</th><th>说明</th><th>是否可改</th></tr>
<tr><td>系统名称 / 标题</td><td>品牌展示名称</td><td>✅ 可改</td></tr>
<tr><td>告警阈值(CPU/内存/磁盘)</td><td>超过阈值触发 Telegram 告警,默认 80%</td><td>✅ 可改</td></tr>
<tr><td>Redis URL</td><td>Redis 连接地址</td><td>✅ 可改</td></tr>
<tr><td>数据库连接池大小</td><td>pool_size / max_overflow</td><td>✅ 可改</td></tr>
<tr><td>Telegram Bot Token / Chat ID</td><td>告警推送配置</td><td>✅ 可改</td></tr>
<tr><td>SECRET_KEY / API_KEY / ENCRYPTION_KEY</td><td>核心安全密钥</td><td>❌ 不可改(需改 .env 重启)</td></tr>
<tr><td>DATABASE_URL</td><td>数据库连接</td><td>❌ 不可改</td></tr>
</table>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> Telegram</div>
<p class="card-desc">配置 Bot Token / Chat ID<strong>测试发送</strong>;通过 getUpdates <strong>检测 Chat ID</strong>;各告警类型独立开关。</p>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> TOTP 与 API Key</div>
<p class="card-desc">TOTP 启用/禁用/<strong>重新绑定</strong>;全局 API Key 需密码二次验证后查看(记审计)。</p>
</div>
<div class="card">
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
<p class="card-desc">总开关、订阅 URL(2h 刷新)、手动 IP、解析预览(见「认证」章节)。</p>
</div>
</div>
<!-- 11. AUDIT -->
<div class="section" id="audit">
<div class="section-title"><span class="icon">📋</span> 11. 审计日志</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 操作审计</div>
<p class="card-desc">所有 CUD 操作(创建/更新/删除服务器、推送、执行脚本、登录、改密码等)自动写入 <code>audit_logs</code>,记录操作人、IP、目标、详情。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「审计」页:日期起止、管理员用户名、操作类型(分组下拉)</li>
<li>每页 50 条分页</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>无日志导出(已否决 CSV/JSON</li>
<li>审计日志不会自动清理,长期需归档</li>
</ul></div>
</div>
</div>
</div>
<!-- 12. INSTALL -->
<div class="section" id="install">
<div class="section-title"><span class="icon">🚀</span> 12. 安装向导</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 5 步安装向导</div>
<p class="card-desc"><code>.env</code> 文件时系统进入安装模式,访问 <code>/app/install.html</code> 完成 5 步配置:欢迎 → 数据库 → Redis → 管理员账号 → 完成。自动生成 API Key / Secret Key / Encryption Key。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>安装完成后 <code>.env</code> 自动生成,安装 API 除 GET /status 外全部锁定(返回 403</li>
<li>重装需手动删除 <code>.env</code> 并重启</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>安装过程未验证数据库/Redis 连接成功才允许进入下一步(应在步骤中手动验证)</li>
<li>安装模式下除 install API 和静态文件外,其余 API 返回 503</li>
</ul></div>
</div>
</div>
</div>
<!-- 13. GUARDIAN -->
<div class="section" id="guardian">
<div class="section-title"><span class="icon">🛡</span> 13. 三层守护机制</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 三层守护</div>
<table class="feat-table">
<tr><th>层级</th><th>机制</th><th>触发条件</th><th>动作</th></tr>
<tr><td>Layer 1</td><td>Supervisor</td><td>Python 进程崩溃</td><td>自动重启</td></tr>
<tr><td>Layer 2</td><td>Python self_monitor30s</td><td>Redis/MySQL 不可达</td><td>发 Telegram 告警/恢复</td></tr>
<tr><td>Layer 3</td><td>Shell cron1 分钟)</td><td>连续 3 次 /health 失败</td><td>supervisorctl restart + Telegram</td></tr>
</table>
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">注意</div><p>Shell cron 依赖宝塔/系统 crontab 配置,部署时需手动添加定时任务。</p></div>
</div>
</div>
<!-- 14. AGENT -->
<div class="section" id="agent">
<div class="section-title"><span class="icon">🤖</span> 14. 子机 Agent</div>
<div class="card">
<div class="card-title"><span class="num">1</span> Agent 功能</div>
<p class="card-desc">部署在每台被管服务器上的轻量服务:<strong>心跳走 443 出站</strong>HTTP 仅本机 <code>127.0.0.1:8601</code>。中心侧脚本已改 <strong>SSH</strong>/exec 端点保留供兼容。</p>
<table class="feat-table">
<tr><th>能力</th><th>说明</th></tr>
<tr><td>心跳</td><td>POST 中心 <code>/api/agent/heartbeat</code>,写 Redis,智能加急</td></tr>
<tr><td>时钟漂移</td><td><code>agent_time</code> 与中心比对,warn/crit + Telegram</td></tr>
<tr><td>IP 白名单</td><td><code>allowed_ips</code> 限制访问本机 Agent HTTP</td></tr>
<tr><td>长任务回调</td><td>POST <code>/api/agent/script-callback</code></td></tr>
<tr><td>安装</td><td><code>web/agent/install.sh</code> — python3.12 venv + systemd</td></tr>
</table>
<div class="info-block warn" style="margin-top:12px"><div class="info-title w">注意</div><ul>
<li>升级 API 存在,建议与 venv 安装脚本对齐后使用</li>
<li>长任务回调需子机出站访问中心 HTTPS</li>
</ul></div>
</div>
</div>
<!-- BACKGROUND -->
<div class="section" id="background">
<div class="section-title"><span class="icon"></span> 后台任务(Primary Worker</div>
<div class="card">
<table class="feat-table">
<tr><th>任务</th><th>间隔</th><th>说明</th></tr>
<tr><td>heartbeat_flush</td><td>10min</td><td>Redis 心跳 → MySQL</td></tr>
<tr><td>self_monitor</td><td>30s</td><td>Redis/MySQL/WS 自检 + Telegram</td></tr>
<tr><td>schedule_runner</td><td>60s</td><td>Cron 调度</td></tr>
<tr><td>retry_runner</td><td>5min</td><td>推送重试</td></tr>
<tr><td>ip_allowlist_refresh</td><td>2h</td><td>订阅 IP 刷新</td></tr>
<tr><td>script_execution_flush</td><td>60s</td><td>执行记录 Redis→MySQL</td></tr>
</table>
</div>
</div>
<!-- ACCEPTANCE -->
<div class="section" id="acceptance">
<div class="section-title"><span class="icon"></span> 标准与验收(与团队同等严谨)</div>
<div class="card">
<p class="card-desc"><strong>全量标准转接</strong><code>docs/project/standards-transfer-package.md</code> — 含 Cursor 规则、<code>standards/</code> 三件套、逐行八步 DoD、设计/上线清单。下一任 AI <strong>不得降低</strong>执行力度。</p>
<table class="feat-table">
<tr><th>类别</th><th>权威路径</th></tr>
<tr><td>标准转接包</td><td><code>docs/project/standards-transfer-package.md</code></td></tr>
<tr><td>验收 L0L5</td><td><code>development-acceptance-standard.md</code></td></tr>
<tr><td>系统开发</td><td><code>standards/system-development-standard.md</code></td></tr>
<tr><td>逐行审计 v2</td><td><code>standards/line-walk-audit-standard-v2.md</code></td></tr>
<tr><td>审计原则</td><td><code>standards/audit-core-principles.md</code></td></tr>
<tr><td>Cursor</td><td><code>.cursor/rules/*.mdc</code>(含 audit-line-review</td></tr>
</table>
<p class="card-desc" style="margin-top:12px">逐行审查:8 步/文件、Closure 全表含 SAFE、FINDING 必须 <code>文件:行号</code>、单回复 ≤5 文件。AI 接续:HANDOFF + 全站功能 + 标准转接包。</p>
</div>
</div>
<!-- 15. NOT IMPL -->
<div class="section" id="notimpl">
<div class="section-title"><span class="icon">🚫</span> 15. 未实现 / 已否决功能</div>
<div class="card">
<table class="feat-table">
<tr><th>功能</th><th>状态</th><th>原因 / 替代方案</th></tr>
<tr><td>文件编辑器(Monaco/ACE</td><td><span class="badge badge-red">已否决</span></td><td>运维场景用 WebSSH + vim/nano;嵌入编辑器增加复杂度无实质收益</td></tr>
<tr><td>会话录像(asciicast</td><td><span class="badge badge-red">已否决</span></td><td>命令日志已满足审计需求</td></tr>
<tr><td>RBAC 权限体系</td><td><span class="badge badge-red">已否决</span></td><td>鉴权即信任,单管理员模型;增加 RBAC 代价远超收益</td></tr>
<tr><td>移动端适配</td><td><span class="badge badge-red">已否决</span></td><td>2000 台服务器运维不会用手机操作</td></tr>
<tr><td>批量服务器导入(CSV</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 API 批量 POST /api/servers/ 实现</td></tr>
<tr><td>文件上传到远程服务器</td><td><span class="badge badge-amber">未实现</span></td><td>可用 WebSSH scp / 推送功能代替</td></tr>
<tr><td>文件内容预览</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 WebSSH cat 查看</td></tr>
<tr><td>WebSSH 断线自动重连</td><td><span class="badge badge-amber">未实现</span></td><td>当前需刷新页面;计划自动重试+按钮</td></tr>
<tr><td>仪表盘历史趋势图</td><td><span class="badge badge-amber">未实现</span></td><td>仅实时 Redis 指标</td></tr>
<tr><td>宝塔 API 监控集成</td><td><span class="badge badge-amber">未实现</span></td><td>可选;参考 btpanel-skills</td></tr>
<tr><td>Agent mTLS</td><td><span class="badge badge-amber">讨论中</span></td><td>未实现</td></tr>
<tr><td>首次部署 E2ET1T5、T-SE</td><td><span class="badge badge-amber">待验证</span></td><td>见 production-verification-checklist.md</td></tr>
</table>
</div>
</div>
<!-- FOOTER -->
<footer>
<p>Nexus 6.0 全站功能说明书 · 2026-05-23(对齐代码库)</p>
<p style="margin-top:6px">
<a href="project/nexus-full-site-features.md">全站功能</a> ·
<a href="project/standards-transfer-package.md">标准转接</a> ·
<a href="project/development-acceptance-standard.md">验收标准</a> ·
<a href="project/AI-HANDOFF-2026-05-23.md">AI 接续</a>
</p>
<p style="margin-top:4px">Python 3.12 / FastAPI 0.115 / MySQL 8.4 / Redis 7 / asyncssh 2.17 / Tailwind v4</p>
</footer>
</div>
</body>
</html>
+75 -85
View File
@@ -1,97 +1,87 @@
# Nexus 文档索引
> 最后更新: 2026-05-22
> 更新: 2026-06-04 · **唯一导航入口**
---
## project/ — 项目概况
| 文件 | 说明 |
## 1. 活跃 SSOT(日常必读)
| 文档 | 说明 |
|------|------|
| `project/status.md` | 项目进度总览 + 数据库结构 + 实施进度 (~85%) |
| `project/roadmap.md` | 功能规划 + ADR架构决策(11项) + 技术债务 |
| `project/deploy.md` | 部署指南 (Ubuntu 20-24, Nginx+Supervisor) |
| `project/tech-stack-inventory.md` | 技术栈清单 + 连接池参数对齐 + v1勘误 |
| [project/nexus-functional-development-guide.md](project/nexus-functional-development-guide.md) | 功能 / API / 14 页 / 部署 |
| [project/nexus-functional-development-guide-appendix.md](project/nexus-functional-development-guide-appendix.md) | sync 端点、调用链、E2E 对照 |
| [project/AI-HANDOFF-2026-06-03.md](project/AI-HANDOFF-2026-06-03.md) | **当前** 会话接续(复制提示词) |
| [project/development-acceptance-standard.md](project/development-acceptance-standard.md) | 验收 L0L5 |
| [project/local-integration-test.md](project/local-integration-test.md) | `bash scripts/local_verify.sh` |
| [project/linux-dev-paths.md](project/linux-dev-paths.md) | Linux 路径 |
| [project/risk-acceptance-single-operator.md](project/risk-acceptance-single-operator.md) | 单人运维风险接受 |
| [project/script-execution.md](project/script-execution.md) | 脚本执行 |
| [project/alert-push-policy.md](project/alert-push-policy.md) | 告警策略 |
| [project/deploy.md](project/deploy.md) | 部署补充(宝塔/裸机) |
| [project/nexus-1panel-operations-knowledge.md](project/nexus-1panel-operations-knowledge.md) | **1Panel + Docker 运维知识 SSOT** |
| [project/nexus-god-menu-audit-summary.md](project/nexus-god-menu-audit-summary.md) | **nx 上帝菜单巡检归档**2026-06-06 round110 |
| [project/production-verification-checklist.md](project/production-verification-checklist.md) | L5 清单 |
## design/ — 设计规格与实施计划
根目录 [AGENTS.md](../AGENTS.md) · [CLAUDE.md](../CLAUDE.md) · [.cursorrules](../.cursorrules) 为薄入口。
### 活跃设计
| 文件 | 说明 |
|------|------|
| `design/specs/design-standards.md` | **设计标准** — 安全模型/性能基线/代码规范/架构模式 |
| `design/specs/2026-05-18-redis-iot-optimization.md` | Redis+实时数据架构 + Key规范 |
| `design/specs/2026-05-17-architecture-optimization.md` | 配置分发与部署架构 |
| `design/specs/2026-05-19-sprint-planning.md` | Sprint规划 — 已完成升级项 + 待实现建议 |
| `design/tech-stack-evaluation.md` | 技术栈评估 + 风险矩阵 + 连接池配置 |
---
### 已归档设计(历史参考)
| 文件 | 说明 |
|------|------|
| `design/specs/2026-05-16-password-presets-design.md` | ⚠️ 归档 — 密码预设设计(引用旧文件结构) |
| `design/specs/2026-05-16-simplified-push-design.md` | ⚠️ 归档 — 简化推送设计(引用旧文件结构) |
| `design/specs/2026-05-17-retry-progress-design.md` | ⚠️ 归档 — 重试+进度设计(引用旧文件结构) |
| `design/specs/2026-05-17-server-add-optimize-design.md` | ⚠️ 归档 — 服务器添加优化设计(引用旧文件结构) |
| `design/specs/2026-05-18-file-manager-v2-design.md` | ⚠️ 归档 — 文件管理器v2设计(引用旧文件结构) |
| `design/specs/2026-05-19-code-review-followup.md` | ⚠️ 归档 — PHP代码审查(前端已迁移到Tailwind) |
| `design/plans/2026-05-16-password-presets.md` | ⚠️ 归档 — 密码预设实施计划 |
| `design/plans/2026-05-16-simplified-push.md` | ⚠️ 归档 — 简化推送实施计划 |
| `design/plans/2026-05-17-retry-progress.md` | ⚠️ 归档 — 重试+进度实施计划 |
| `design/plans/2026-05-17-server-add-optimize.md` | ⚠️ 归档 — 服务器添加优化实施计划 |
| `design/plans/2026-05-18-file-manager-v2.md` | ⚠️ 归档 — 文件管理器v2实施计划 |
## 2. 变更与审计
## reports/ — 审查与工作报告
### 部门审查报告
| 文件 | 说明 |
|------|------|
| `reports/review-cto-2026-05-21.md` | CTO审查 — 连接池/SSH/CORS/依赖(含勘误) |
| `reports/review-deploy-architecture-2026-05-21.md` | 运维总监审查 — 部署架构/三层守护/Nginx(含勘误) |
| `reports/review-quality-director-2026-05-21.md` | 质量总监审查 — 代码规范/datetime/测试(含勘误) |
| `reports/step-0-infrastructure/ecc-security-report.md` | ECC组安全审查 — Redis/Schema/认证/SSH/编码(含勘误) |
| `reports/signoff-2026-05-21.md` | 审查签字确认 |
| `reports/session-log-2026-05-21.md` | 会话操作日志 |
### 实施报告
| 文件 | 说明 |
|------|------|
| `reports/implementation-progress-2026-05-21.md` | 6步实施进度总览 |
| `reports/step-0-infrastructure/engineering-report.md` | Step 0: 基础设施加固 |
| `reports/step-0-infrastructure/ag-code-review-report.md` | Step 0: 代码审查 |
| `reports/step-0-infrastructure/testing-report.md` | Step 0: 测试报告 |
| `reports/step-1-data-layer/engineering-report.md` | Step 1: 数据层 |
| `reports/step-2-auth/engineering-report.md` | Step 2: 认证层 |
| `reports/step-3-webssh/engineering-report.md` | Step 3: Web SSH(含paramiko→asyncssh迁移) |
| `reports/step-4-sync/engineering-report.md` | Step 4: Sync引擎 |
| `reports/step-5-frontend/engineering-report.md` | Step 5: 前端迁移 |
| `reports/step-5-frontend/design-review-report.md` | Step 5: 前端设计审查 |
| `reports/step-5-frontend/product-department-report.md` | Step 5: 产品部报告 |
## research/ — 竞品与技术调研
| 文件 | 说明 |
|------|------|
| `research/jumpserver-easynode-architecture-analysis.md` | JumpServer & EasyNode 架构对比分析 |
| `research/ag-research-report.md` | AI Agent 调研报告 |
## changelog/ — 修复记录
| 文件 | 说明 |
|------|------|
| `changelog/fix-plan-2026-05-20.md` | 代码问题修复计划(13项) |
| `changelog/fix-plan-full-2026-05-20.md` | 全量未完成项修复计划(24项) |
| `changelog/fix-verification-report-2026-05-20.md` | 修复验证报告 |
## memory/ — 项目记忆(AI辅助)
| 文件 | 说明 |
|------|------|
| `memory/MEMORY.md` | 记忆索引 |
| `memory/mem_*.md` | 17个记忆文件(概述/决策/数据流/守护/模块/测试/等) |
## team/ — 部门协作
| 文件 | 说明 |
|------|------|
| `team/collaboration-charter.md` | 部门协作章程(5阶段流水线) |
| `team/roster-2026-05-21.md` | 团队编制档案(194人/9部门) |
| `team/roles/` | 12个部门角色定义文档 |
## skills/ — 团队技能定义(AI辅助)
| 目录 | 说明 |
|------|------|
| `skills/` | 194个技能文件(各部门AI技能定义) |
| [changelog/](changelog/) | 每次代码变更(**不归档、不合并**)· [INDEX](changelog/INDEX.md) |
| [audit/](audit/) | 门控用审计 · [INDEX](audit/INDEX.md) · `2026-06-04-bug-scan-registry.json` |
---
## 3. 活跃报告(`reports/`
| 文件 | 说明 |
|------|------|
| [audit-plugin-run-final-2026-06-04.md](reports/audit-plugin-run-final-2026-06-04.md) | 插件审计总验收 |
| [audit-frontend-14p-2026-06-04-closure.md](reports/audit-frontend-14p-2026-06-04-closure.md) | 前端 14 页 closure |
| [audit-phase-4-plugin-closure-2026-06-04.md](reports/audit-phase-4-plugin-closure-2026-06-04.md) | Phase4 收尾 |
| [2026-06-01-production-verification.md](reports/2026-06-01-production-verification.md) | 生产验证(待更新 L5 |
| [2026-06-01-final-acceptance-checklist.md](reports/2026-06-01-final-acceptance-checklist.md) | 终验清单 |
批量 batch 报告 → [archive/audits/](archive/audits/) 合并卷。
---
## 4. 测试(全站用例 SSOT
| 文档 | 说明 |
|------|------|
| [testing/test-case-matrix.md](testing/test-case-matrix.md) | **152 条** UT/IT/CH/E2E/MCP 编号用例 |
| [testing/mcp-browser-playbook.md](testing/mcp-browser-playbook.md) | Agent Playwright MCP 点页规程 |
| [design/specs/2026-06-08-full-site-testing-strategy-design.md](design/specs/2026-06-08-full-site-testing-strategy-design.md) | 四层金字塔设计 |
| [design/plans/2026-06-08-full-site-testing-strategy.md](design/plans/2026-06-08-full-site-testing-strategy.md) | 分阶段实施计划 |
---
## 5. 设计与标准
| 目录 | 说明 |
|------|------|
| [design/specs/](design/specs/) | 设计规格 |
| [design/plans/](design/plans/) | 实施计划 |
| [../standards/](../standards/) | 开发与审计标准 |
| [project/standards-transfer-package.md](project/standards-transfer-package.md) | 标准转接包 |
---
## 6. 归档区
**[archive/README.md](archive/README.md)** — 2026-06 审计合并卷、旧 handoff、agentmemory、2026-05 审查报告。
---
## 7. 工具脚本
| 脚本 | 用途 |
|------|------|
| `scripts/local_verify.sh` | L2b 本地验证 |
| `scripts/consolidate_docs.py` | 文档归档合并(维护用) |
| `scripts/run_nexus_acceptance.py` | L4 验收 |
| `deploy/pre_deploy_check.sh` | 7 道门控 |
+17
View File
@@ -0,0 +1,17 @@
# Nexus 文档归档区
> 更新: 2026-06-04
历史审计、旧 handoff、agentmemory。**日常请先读** [docs/README.md](../README.md)。
## 合并卷
| 文件 | 内容 |
|------|------|
| [audits/2026-06-04-line-walk-merged.md](audits/2026-06-04-line-walk-merged.md) | Line-walk batch |
| [audits/2026-06-04-delta-merged.md](audits/2026-06-04-delta-merged.md) | Delta D1D8 |
| [audits/2026-06-04-phase2-merged.md](audits/2026-06-04-phase2-merged.md) | Phase2 P2 |
| [audits/2026-06-04-frontend-14p-merged.md](audits/2026-06-04-frontend-14p-merged.md) | 前端 14p |
| [memory/agentmemory-2026-05-merged.md](memory/agentmemory-2026-05-merged.md) | 旧 memory |
原文在对应子目录;changelog 不归档。
@@ -0,0 +1,183 @@
# Delta D1D8 合并卷
> 生成日期: 2026-06-04 · 源文件 8 篇 · 原文 `docs/archive/audits/2026-06-04-delta/`
## 批次索引
### `audit-delta-2026-06-04-waveD1.md`
# Phase 1 Delta 审计 — Wave D1(同步/后台)
## 登记
## Step 3 — 规则扫描 H(摘要)
## 入口表(sync_v2.py API
## Closure 表
**len(H)=19, Closure=19**
## FINDING(本波)
无 P0/P1 新 FINDING。RISK 项与既有运维/单人决策一致,不阻塞。
## DoD
- [x] len(H) == Closure
- [x] FINDING 含行号
## 验证
### `audit-delta-2026-06-04-waveD2.md`
# Phase 1 Delta 审计 — Wave D2(认证 / 安装 / WS / Agent
## 登记
## 入口表
### install.py
### auth.py
### websocket.py
### agent.py
## Closure 表
**len(H)=22, Closure=22**
## FINDING
无 P0/P1 新 FINDING。RISKWS query JWT、Agent global key — 见 `docs/project/risk-acceptance-single-operator.md`
## DoD
### `audit-delta-2026-06-04-waveD3.md`
# Phase 1 Delta 审计 — Wave D3(服务器 / 设置 / 脚本 / WebSSH
## 登记
## 入口表(摘要)
### webssh.py
### servers.py(主要写操作)
### settings.py
### scripts.py
## Closure 表
**len(H)=24, Closure=24**
## FINDING
无 P0/P1 新 FINDING。
## DoD
### `audit-delta-2026-06-04-waveD4.md`
# Phase 1 Delta 审计 — Wave D4(前端 Push
## 登记
## Step 3 — 规则扫描 H(摘要)
## 入口表(前端 → API
## Closure 表
**len(H)=17, Closure=17**
## FINDING
无 P0/P1 新 FINDING。
## DoD
- [x] len(H) == Closure
## 验证
### `audit-delta-2026-06-04-waveD5.md`
# Phase 1 Delta 审计 — Wave D5(前端 Files / Terminal
## 登记
## Step 3 — 规则扫描 H(摘要)
## 入口表(前端 → API / WS
## Closure 表
**len(H)=18, Closure=18**
## FINDING
无 P0/P1 新 FINDING。
## DoD
- [x] len(H) == Closure
## 验证
### `audit-delta-2026-06-04-waveD6.md`
# Phase 1 Delta 审计 — Wave D6(认证链 / Login
## 登记
## Step 3 — 规则扫描 H(摘要)
## 入口表
## Closure 表
**len(H)=19, Closure=19**
## FINDING
无 P0/P1 新 FINDING。
## DoD
- [x] len(H) == Closure
## 验证
### `audit-delta-2026-06-04-waveD7.md`
# Phase 1 Delta 审计 — Wave D7Deploy / 本地验证脚本)
**标准**: 变更审查(运维/门控;非业务 API 8 步,沿用 Closure 表格式)
## 登记
## Step 3 — 规则扫描 H(摘要)
## 入口表(脚本调用链)
## Closure 表
**len(H)=16, Closure=16**
## FINDING
无 P0/P1 新 FINDING。
## DoD
- [x] len(H) == Closure
## 验证
### `audit-delta-2026-06-04-waveD8-closure.md`
# Phase 1 Delta 审计 — Wave D8(汇总 Closure
**标准**: Phase 1 收尾 DoD
## 汇总表
| 波次 | 主题 | 文件数 | H | P0/P1 | 报告 |
## 已接受 RISK(跨波重复,不重复开 FINDING)
## Registry 更新
## Step 3 / Closure / DoD
- **len(H) 汇总** = 135,各波 Closure 均已 100% 对齐
- **无 P0/P1** 待修项进入 Phase 2
## 下一步(计划 Phase 2 / L3
## 验证
## Closure 摘录(合并)
| 1 | `server/api/sync_v2.py` | Python | 1656 | 1200, 6801080, 12801657 + 全文件 `@router`/grep H |
| 2 | `server/application/services/sync_engine_v2.py` | Python | 552 | 1552 全文 |
| 3 | `server/background/schedule_runner.py` | Python | 262 | 1262 全文 |
| 4 | `server/background/retry_runner.py` | Python | 169 | 1169 全文 |
| 5 | `server/background/heartbeat_flush.py` | Python | 101 | 1101 全文 |
| H | 文件:行号 | 判定 | 理由 |
| H-Install-lock | install.py:344-350 | SAFE | `_reject_if_locked` 阻断突变 |
| H-Install-token | install.py:314-327 | SAFE | `secrets.compare_digest` 一次性 token |
| H-Install-token | install.py:601 | SAFE | create-admin 必须 token |
| H-Install-SQL | install.py:540-544 | SAFE | 参数化 INSERT settings |
| H-Install-DDL | install.py:489-503 | SAFE | DDL 幂等 except pass |
| H-Install-subprocess | install.py:273-293 | SAFE | crontab/supervisor 固定 argv |
| H-Auth-brute | auth.py:125-138 | SAFE | 429/202 由 AuthService |
| H-Auth-cookie | auth.py:38-54 | SAFE | HttpOnly + Secure(HTTPS) + SameSite=Lax |
| H-Auth-TOTP-IDOR | auth.py:223-259 | SAFE | admin_id 必须等于 JWT sub |
| H-Auth-password | auth.py:288-305 | SAFE | bcrypt + token_version 失效 |
| H-Auth-webssh | auth.py:330-349 | SAFE | 短效 server-bound token |
| H-JWT-middleware | auth_jwt.py:78-146 | SAFE | ASGI 层拦截 /api/* |
| H-JWT-public | auth_jwt.py:38-50 | SAFE | 公开前缀含 agent/install/ws |
| H-JWT-tv | auth_jwt.py:222-226 | SAFE | token_version 精确匹配 |
| H-WS-auth | websocket.py:303-311 | SAFE | 无 token → 4001;无效 → 4401 |
| H-WS-auth | websocket.py:366-407 | SAFE | tv + updated_at 双检 |
| H-WS-ADR011 | websocket.py:9,300 | RISK | JWT 在 query;单人运维已接受 ADR-011 |
| H-Agent-key | agent.py:40-57 | SAFE | compare_digesthandler 二次验 per-server |
| H-Agent-unknown | agent.py:193-199 | SAFE | 未知 server 200 discard,不写入 |
| H-Agent-global | agent.py:84-92 | RISK | legacy global API_KEYrisk-acceptance 文档 |
| H-Agent-callback | agent.py:324-347 | SAFE | rate limit + job secret |
| H-Agent-no-exec | agent.py:378 | SAFE | 控制面无 shell exec |
| POST | `/{id}/install-agent` 等 | JWT + SSH |
| 文件 | H 命中 |
| usePushForm.ts | H-Auth, H-Upload, H-Path |
| usePushPreview.ts | H-Auth, H-Path |
| usePushProgress.ts | H-Auth, H-WS, H-Batch |
| PushZipUpload.vue | H-Upload, H-XSS |
| PushServerGrid.vue | H-XSS |
| useFilesActions.ts | H-Auth, H-Path, H-Delete, H-Upload |
| useFilesEditor.ts | H-Auth, H-Size |
| useFilesPermissions.ts | H-Auth, H-Sudo |
| useTerminalSessions.ts | H-Auth, H-WS, H-Path, H-Cmd |
| useTerminalCmdBar.ts | H-Storage, H-Cmd |
| LoginPage.vue | H-Secret, H-Redirect, H-Lockout, H-XSS |
| auth.ts | H-Token, H-Refresh, H-Profile |
| api/index.ts | H-Auth, H-Refresh, H-401 |
| router/index.ts | H-Guard, H-Redirect |
| wsUrl.ts | H-WS |
| pre_deploy_check.sh | H-Gate, H-Secret, H-Git |
| local_verify.sh | H-Chain |
| local_integration_smoke.sh | H-Health, H-Auth |
| start-dev.sh | H-DevBind, H-Env |
| docker-compose.yml | H-Port, H-DefaultCreds |
| 波次 | 主题 | 文件数 | H | P0/P1 | 报告 |
| D1 | sync/后台 | 5 | 19 | 0 | [waveD1](audit-delta-2026-06-04-waveD1.md) |
| D2 | auth/agent/ws | 5 | 22 | 0 | [waveD2](audit-delta-2026-06-04-waveD2.md) |
| D3 | servers/settings/scripts/webssh | 5 | 24 | 0 | [waveD3](audit-delta-2026-06-04-waveD3.md) |
| D4 | 前端 Push | 5 | 17 | 0 | [waveD4](audit-delta-2026-06-04-waveD4.md) |
| D5 | 前端 Files/Terminal | 5 | 18 | 0 | [waveD5](audit-delta-2026-06-04-waveD5.md) |
| D6 | 认证链 Login | 5 | 19 | 0 | [waveD6](audit-delta-2026-06-04-waveD6.md) |
| D7 | Deploy/本地脚本 | 5 | 16 | 0 | [waveD7](audit-delta-2026-06-04-waveD7.md) |
| **合计** | | **35** | **135** | **0** | |
@@ -0,0 +1,95 @@
# Phase 1 Delta 审计 — Wave D1(同步/后台)
**日期**: 2026-06-04
**范围**: 推送/sync 核心路径(Code Review batch1 后 delta
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
## 登记
| # | 文件 | 语言 | 行数 N | Read 范围 |
|---|------|------|--------|-----------|
| 1 | `server/api/sync_v2.py` | Python | 1656 | 1200, 6801080, 12801657 + 全文件 `@router`/grep H |
| 2 | `server/application/services/sync_engine_v2.py` | Python | 552 | 1552 全文 |
| 3 | `server/background/schedule_runner.py` | Python | 262 | 1262 全文 |
| 4 | `server/background/retry_runner.py` | Python | 169 | 1169 全文 |
| 5 | `server/background/heartbeat_flush.py` | Python | 101 | 1101 全文 |
## Step 3 — 规则扫描 H(摘要)
| 文件 | H 命中 |
|------|--------|
| sync_v2.py | H-Shell, H-Auth, H-Path, H-Audit, H-Upload |
| sync_engine_v2.py | H-Shell, H-AuthN/A, H-Concurrency |
| schedule_runner.py | H-Cron, H-Script-exec, H-Lock |
| retry_runner.py | H-Lock, H-Retry |
| heartbeat_flush.py | H-Redis, H-DB |
## 入口表(sync_v2.py API
| 方法 | 路径 | 鉴权 |
|------|------|------|
| POST | `/api/sync/files` | JWT `get_current_admin` |
| POST | `/api/sync/preview` | 同上 |
| POST | `/api/sync/file-ops` | 同上 |
| POST | `/api/sync/file-clipboard` | 同上 |
| POST | `/api/sync/browse` | 同上 |
| POST | `/api/sync/browse-local` | 同上 |
| POST | `/api/sync/local-file-ops` | 同上 |
| POST | `/api/sync/local-file-preview` | 同上 |
| POST | `/api/sync/cancel` | 同上 |
| POST | `/api/sync/reconcile-stale-logs` | 同上 |
| POST | `/api/sync/validate-source-path` | 同上 |
| POST | `/api/sync/diagnose` | 同上 |
| POST | `/api/sync/file-diff` | 同上 |
| POST | `/api/sync/upload` | 同上 |
| POST | `/api/sync/upload-zip` | 同上 |
| POST | `/api/sync/verify` | 同上 |
| POST | `/api/sync/download` | 同上 |
| POST | `/api/sync/read-file` | 同上 |
| POST | `/api/sync/write-file` | 同上 |
| POST | `/api/sync/chmod` | 同上 |
| POST | `/api/sync/compress` | 同上 |
| POST | `/api/sync/decompress` | 同上 |
## Closure 表
| H | 文件:行号 | 判定 | 理由 |
|---|-----------|------|------|
| H-Shell | sync_v2.py:156-173 | SAFE | 远程路径 `normalize_remote_abs_path` + `shlex.quote` |
| H-Shell | sync_v2.py:777 | SAFE | `remote_file``remote_join`+quote`wc -c && cat` 单路径 |
| H-Path | sync_v2.py:721-731 | SAFE | `ensure_under_nexus_upload` + prefix 校验 |
| H-Path | sync_v2.py:887-890 | SAFE | 上传文件名去斜杠 |
| H-Path | sync_v2.py:999-1005 | SAFE | ZIP slip`assert_zip_member_safe` |
| H-Auth | sync_v2.py:74-1610 | SAFE | 全部 22 路由 `Depends(get_current_admin)` |
| H-Audit | sync_v2.py:119-122 等 | SAFE | CUD/敏感操作 `_audit_sync` |
| H-Upload | sync_v2.py:876-877 | SAFE | 100MB 上限 |
| H-Upload | sync_v2.py:977-978 | SAFE | ZIP 500MB 上限 |
| H-Shell | sync_engine_v2.py:433-465 | RISK | `StrictHostKeyChecking=no` 运维惯例;单人环境已接受 |
| H-Shell | sync_engine_v2.py:440-445 | RISK | sshpass/SSHPASS;内部工具,凭据加密存储 |
| H-Concurrency | sync_engine_v2.py:89-150 | SAFE | `_db_lock` 串行化 sync_log 写 |
| H-Cron | schedule_runner.py:58-72 | RISK | 畸形 cron 字段 `int()` 可抛 ValueError;外层 254 行捕获 rollback |
| H-Script | schedule_runner.py:198-205 | SAFE | 调度脚本仅 JWT 管理员配置的 schedule 触发 |
| H-Lock | schedule_runner.py:125-131 | SAFE | `FOR UPDATE SKIP LOCKED` 防双触发 |
| H-Lock | retry_runner.py:67-76 | SAFE | SKIP LOCKED 认领 |
| H-Retry | retry_runner.py:22-59 | SAFE | 指数退避 + max_retries |
| H-Redis | heartbeat_flush.py:42-48 | SAFE | scan 失败 log+continue |
| H-DB | heartbeat_flush.py:57-99 | SAFE | 参数化 ORM update;未知 server_id rowcount 0 跳过 |
**len(H)=19, Closure=19**
## FINDING(本波)
无 P0/P1 新 FINDING。RISK 项与既有运维/单人决策一致,不阻塞。
## DoD
- [x] len(H) == Closure
- [x] Read 覆盖 1..Nsync_v2 大文件分段 + 全路由 grep)
- [x] FINDING 含行号
- [x] 入口表 + sink 标注
- [x] 逐行完成 ✓(D1 五文件)
## 验证
- `docker-compose.yml` Redis 发布 6379(修复本地 uvicorn 连 Redis
- 待跑:`bash scripts/local_verify.sh`
@@ -0,0 +1,100 @@
# Phase 1 Delta 审计 — Wave D2(认证 / 安装 / WS / Agent
**日期**: 2026-06-04
**范围**: Code Review batch2 后 delta — 认证与 Agent 路径
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
## 登记
| # | 文件 | 语言 | 行数 N | Read 范围 |
|---|------|------|--------|-----------|
| 1 | `server/api/install.py` | Python | 746 | 1746 全文 |
| 2 | `server/api/auth.py` | Python | 374 | 1374 全文 |
| 3 | `server/api/auth_jwt.py` | Python | 274 | 1274 全文 |
| 4 | `server/api/websocket.py` | Python | 552 | 1552 全文 |
| 5 | `server/api/agent.py` | Python | 378 | 1378 全文 |
## 入口表
### install.py
| 方法 | 路径 | 鉴权 |
|------|------|------|
| GET | `/api/install/status` | 无(已锁定 404 |
| GET | `/api/install/env-check` | 安装模式 + 未 lock |
| POST | `/api/install/init-db` | 无 .env + 未 lock |
| POST | `/api/install/create-admin` | `install_token` |
| POST | `/api/install/lock` | 需已有 admin |
| GET | `/api/install/state` | 未 lock |
### auth.py
| 方法 | 路径 | 鉴权 |
|------|------|------|
| POST | `/api/auth/login` | 公开 |
| POST | `/api/auth/refresh` | Cookie/body refresh |
| POST | `/api/auth/logout` | Cookie |
| POST | `/api/auth/totp/*` | JWT + self admin_id |
| PUT | `/api/auth/password` | JWT + 当前密码 (+TOTP) |
| POST | `/api/auth/webssh-token` | JWT + server 存在 |
| GET | `/api/auth/me` | JWT |
### websocket.py
| 类型 | 路径 | 鉴权 |
|------|------|------|
| WS | `/ws/alerts` | Query `?token=` JWT |
| WS | `/ws/sync` | 同上 |
### agent.py
| 方法 | 路径 | 鉴权 |
|------|------|------|
| POST | `/api/agent/heartbeat` | X-API-Key + per-server key |
| POST | `/api/agent/script-callback` | X-API-Key + job secret + rate limit |
## Closure 表
| H | 文件:行号 | 判定 | 理由 |
|---|-----------|------|------|
| H-Install-lock | install.py:344-350 | SAFE | `_reject_if_locked` 阻断突变 |
| H-Install-token | install.py:314-327 | SAFE | `secrets.compare_digest` 一次性 token |
| H-Install-token | install.py:601 | SAFE | create-admin 必须 token |
| H-Install-SQL | install.py:540-544 | SAFE | 参数化 INSERT settings |
| H-Install-DDL | install.py:489-503 | SAFE | DDL 幂等 except pass |
| H-Install-subprocess | install.py:273-293 | SAFE | crontab/supervisor 固定 argv |
| H-Auth-brute | auth.py:125-138 | SAFE | 429/202 由 AuthService |
| H-Auth-cookie | auth.py:38-54 | SAFE | HttpOnly + Secure(HTTPS) + SameSite=Lax |
| H-Auth-TOTP-IDOR | auth.py:223-259 | SAFE | admin_id 必须等于 JWT sub |
| H-Auth-password | auth.py:288-305 | SAFE | bcrypt + token_version 失效 |
| H-Auth-webssh | auth.py:330-349 | SAFE | 短效 server-bound token |
| H-JWT-middleware | auth_jwt.py:78-146 | SAFE | ASGI 层拦截 /api/* |
| H-JWT-public | auth_jwt.py:38-50 | SAFE | 公开前缀含 agent/install/ws |
| H-JWT-tv | auth_jwt.py:222-226 | SAFE | token_version 精确匹配 |
| H-WS-auth | websocket.py:303-311 | SAFE | 无 token → 4001;无效 → 4401 |
| H-WS-auth | websocket.py:366-407 | SAFE | tv + updated_at 双检 |
| H-WS-ADR011 | websocket.py:9,300 | RISK | JWT 在 query;单人运维已接受 ADR-011 |
| H-Agent-key | agent.py:40-57 | SAFE | compare_digesthandler 二次验 per-server |
| H-Agent-unknown | agent.py:193-199 | SAFE | 未知 server 200 discard,不写入 |
| H-Agent-global | agent.py:84-92 | RISK | legacy global API_KEYrisk-acceptance 文档 |
| H-Agent-callback | agent.py:324-347 | SAFE | rate limit + job secret |
| H-Agent-no-exec | agent.py:378 | SAFE | 控制面无 shell exec |
**len(H)=22, Closure=22**
## FINDING
无 P0/P1 新 FINDING。RISKWS query JWT、Agent global key — 见 `docs/project/risk-acceptance-single-operator.md`
## DoD
- [x] len(H) == Closure
- [x] Read 1..N 全文
- [x] 入口表 + sink
- [x] 逐行完成 ✓(D2 五文件)
## 验证
```bash
bash scripts/local_verify.sh # 预期全绿
```
@@ -0,0 +1,85 @@
# Phase 1 Delta 审计 — Wave D3(服务器 / 设置 / 脚本 / WebSSH
**日期**: 2026-06-04
**范围**: Code Review batch23 相关路径
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
## 登记
| # | 文件 | 语言 | 行数 N | Read 范围 |
|---|------|------|--------|-----------|
| 1 | `server/api/webssh.py` | Python | 398 | 1398 全文 |
| 2 | `server/api/servers.py` | Python | 1741 | 1250, 10591290, 16811742 + 全路由 grep |
| 3 | `server/api/settings.py` | Python | 1403 | 1130, 280384, 562730, 881930 + grep |
| 4 | `server/application/services/script_service.py` | Python | 734 | 1734 全文 |
| 5 | `server/api/scripts.py` | Python | 418 | 1418 全文 |
## 入口表(摘要)
### webssh.py
| 类型 | 路径 | 鉴权 |
|------|------|------|
| WS | `/ws/terminal/{server_id}` | JWT `purpose=webssh` + server_id 绑定 |
### servers.py(主要写操作)
| 方法 | 路径 | 鉴权 |
|------|------|------|
| POST/PUT/DELETE | `/api/servers/`… | JWT |
| POST | `/{id}/agent-key` | JWT + 当前密码 |
| POST | `/{id}/install-agent` 等 | JWT + SSH |
### settings.py
| 方法 | 路径 | 鉴权 |
|------|------|------|
| GET/PUT | `/api/settings/*` | JWTIMMUTABLE_KEYS 403 |
| POST | `/api-key/reveal`, `/telegram/reveal-token` | JWT + re-auth |
### scripts.py
| 方法 | 路径 | 鉴权 |
|------|------|------|
| CRUD/exec | `/api/scripts/*` | JWT 全路由 |
## Closure 表
| H | 文件:行号 | 判定 | 理由 |
|---|-----------|------|------|
| H-WS-auth | webssh.py:96-109 | SAFE | webssh JWT + server_id 必须匹配路径 |
| H-WS-IDOR | webssh.py:106-108 | SAFE | 非绑定 token → 4003 |
| H-WS-cred | webssh.py:127-135 | SAFE | SSH 凭据未配置则拒绝 |
| H-WS-audit | webssh.py:137-157 | SAFE | connect/disconnect 审计 |
| H-WS-cmdlog | webssh.py:276-290 | SAFE | 命令日志 + dangerous check |
| H-WS-ADR011 | webssh.py:92 | RISK | query token;已接受 |
| H-Server-mask | servers.py:1692-1698 | SAFE | password_set / 密钥不返回明文 |
| H-Server-encrypt | servers.py:1096-1099 | SAFE | Fernet 加密存储 |
| H-Server-allowlist | servers.py:1163-1187 | SAFE | UPDATE 字段白名单 |
| H-Server-agent-key | servers.py:1251-1288 | SAFE | re-auth 后生成/返回一次 |
| H-Server-shell | servers.py:631-633 | SAFE | install cmd shlex.quote |
| H-Server-sudo | servers.py:36-60 | RISK | 临时 sudoers;运维必要 |
| H-Settings-immutable | settings.py:331-334 | SAFE | api_key 等不可 PUT |
| H-Settings-mask | settings.py:114-128 | SAFE | GET 敏感项 value 空 + value_set |
| H-Settings-reauth | settings.py:93-98 | SAFE | reveal 需 bcrypt |
| H-Settings-SSRF | settings.py:68-88 | SAFE | 订阅 URL 私有 IP 拦截 |
| H-Settings-PUT-leak | settings.py:383 | RISK | PUT 响应可能含 telegram 明文;JWT 已认证 |
| H-Script-danger | scripts.py:352-359 | SAFE | check_dangerous_command 拒绝 |
| H-Script-cred | scripts.py:407-418 | SAFE | 凭据 API 无 password 字段 |
| H-Script-allowlist | scripts.py:111-114,303-306 | SAFE | credential/script UPDATE 白名单 |
| H-Svc-redact | script_service.py:108-111 | SAFE | 执行记录 command 脱敏 DB_PASS |
| H-Svc-ssh | script_service.py:680-698 | SAFE | SSH exec 经 pool |
| H-Svc-kill | script_service.py:306-322 | SAFE | kill 命令 pid/log 路径校验 |
| H-Svc-server-map | script_service.py:519-523 | SAFE | 预加载 server_map 避免并行竞态 |
**len(H)=24, Closure=24**
## FINDING
无 P0/P1 新 FINDING。
## DoD
- [x] len(H) == Closure
- [x] Read 覆盖(大文件分段 + 全路由)
- [x] 逐行完成 ✓(D3 五文件)
## 验证
`bash scripts/local_verify.sh` — 预期全绿
@@ -0,0 +1,74 @@
# Phase 1 Delta 审计 — Wave D4(前端 Push
**日期**: 2026-06-04
**范围**: Code Review 后 push 页 composables + 组件
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
## 登记
| # | 文件 | 语言 | 行数 N | Read 范围 |
|---|------|------|--------|-----------|
| 1 | `frontend/src/composables/push/usePushForm.ts` | TS | 101 | 1101 全文 |
| 2 | `frontend/src/composables/push/usePushPreview.ts` | TS | 104 | 1104 全文 |
| 3 | `frontend/src/composables/push/usePushProgress.ts` | TS | 294 | 1294 全文 |
| 4 | `frontend/src/components/push/PushZipUpload.vue` | Vue | 51 | 151 全文 |
| 5 | `frontend/src/components/push/PushServerGrid.vue` | Vue | 110 | 1110 全文 |
**关联(本波未单独登记)**: `usePushPage.ts` 仅编排 lifecycle,无独立 sink。
## Step 3 — 规则扫描 H(摘要)
| 文件 | H 命中 |
|------|--------|
| usePushForm.ts | H-Auth, H-Upload, H-Path |
| usePushPreview.ts | H-Auth, H-Path |
| usePushProgress.ts | H-Auth, H-WS, H-Batch |
| PushZipUpload.vue | H-Upload, H-XSS |
| PushServerGrid.vue | H-XSS |
## 入口表(前端 → API
| 调用方 | 方法 | 路径 | 鉴权 |
|--------|------|------|------|
| usePushForm | POST multipart | `/sync/upload-zip` | http JWT |
| usePushPreview | POST | `/sync/preview` | http JWT |
| usePushProgress | POST | `/sync/files`, `/sync/cancel` | http JWT |
| usePushProgress | WS | `/ws/sync?token=` | JWT query(已接受 RISK |
## Closure 表
| H | 文件:行号 | 判定 | 理由 |
|---|-----------|------|------|
| H-Auth | usePushForm.ts:65-67 | SAFE | 经 `@/api` httpBearer JWT |
| H-Upload | usePushForm.ts:52,57 | RISK | 客户端仅 `.zip` 后缀;服务端 ZIP slip/大小限制(D1 |
| H-Path | usePushForm.ts:69-72 | SAFE | `source_path` 来自服务端 upload-zip 响应 |
| H-Path | usePushForm.ts:38-40 | SAFE | target_path 必填;远程校验在 sync_v2 |
| H-Auth | usePushPreview.ts:28-38 | SAFE | preview POST JWT + server_id 参数化 |
| H-Concurrency | usePushPreview.ts:72-77 | SAFE | `PREVIEW_CONCURRENCY=4` 限流 |
| H-Auth | usePushProgress.ts:237-243 | SAFE | push POST JWT + batch_id |
| H-WS | usePushProgress.ts:137-142 | RISK | query token;单人运维已接受 |
| H-WS | usePushProgress.ts:79-80 | SAFE | batch_id 过滤防串批 |
| H-WS | usePushProgress.ts:165-167 | SAFE | 4401 → forceLogout |
| H-Batch | usePushProgress.ts:244-246 | SAFE | 服务端 batch_id 可覆盖客户端 |
| H-Cancel | usePushProgress.ts:194-209 | SAFE | cancel 带 batch_id |
| H-Timer | usePushProgress.ts:261-275 | SAFE | 300s 安全超时 + dispose 清理 |
| H-Secret | usePushForm/Preview/Progress | SAFE | 无 localStorage 密码/密钥 |
| H-XSS | PushZipUpload.vue:22 | SAFE | `{{ uploadedZip.name }}` 文本插值 |
| H-XSS | PushServerGrid.vue:66-77 | SAFE | 无 v-html;状态 chip 文本插值 |
| H-Upload | PushZipUpload.vue:18 | SAFE | `accept=".zip"` + 父 composable 后缀检查 |
**len(H)=17, Closure=17**
## FINDING
无 P0/P1 新 FINDING。
## DoD
- [x] len(H) == Closure
- [x] 五文件全文 Read
- [x] 无 v-html / localStorage 凭据
## 验证
`bash scripts/local_verify.sh` — 全绿
@@ -0,0 +1,80 @@
# Phase 1 Delta 审计 — Wave D5(前端 Files / Terminal
**日期**: 2026-06-04
**范围**: files/terminal composablesCode Review 后 delta
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
## 登记
| # | 文件 | 语言 | 行数 N | Read 范围 |
|---|------|------|--------|-----------|
| 1 | `frontend/src/composables/files/useFilesActions.ts` | TS | 609 | 1609 全文 |
| 2 | `frontend/src/composables/files/useFilesEditor.ts` | TS | 163 | 1163 全文 |
| 3 | `frontend/src/composables/files/useFilesPermissions.ts` | TS | 101 | 1101 全文 |
| 4 | `frontend/src/composables/terminal/useTerminalSessions.ts` | TS | 749 | 1749 全文 |
| 5 | `frontend/src/composables/terminal/useTerminalCmdBar.ts` | TS | 108 | 1108 全文 |
**关联**: `types.ts``sanitizeRemotePath`4043)已纳入 terminal 路径 H 判定。
## Step 3 — 规则扫描 H(摘要)
| 文件 | H 命中 |
|------|--------|
| useFilesActions.ts | H-Auth, H-Path, H-Delete, H-Upload |
| useFilesEditor.ts | H-Auth, H-Size |
| useFilesPermissions.ts | H-Auth, H-Sudo |
| useTerminalSessions.ts | H-Auth, H-WS, H-Path, H-Cmd |
| useTerminalCmdBar.ts | H-Storage, H-Cmd |
## 入口表(前端 → API / WS
| 调用方 | 方法 | 路径 | 鉴权 |
|--------|------|------|------|
| useFilesActions | POST | `/sync/file-ops`, `/sync/write-file`, `/sync/chmod`, `/sync/compress`, `/sync/decompress`, `/sync/upload`, `/sync/download` | http JWT |
| useFilesEditor | 经 `readRemoteFileContent` | `/sync/read-file` | http JWT |
| useFilesPermissions | GET/POST | `/servers/{id}/files-capability`, `setup-files-sudo` | http JWT |
| useTerminalSessions | POST | `/auth/webssh-token` | http JWT |
| useTerminalSessions | WS | `/ws/terminal/{id}?token=` | purpose=webssh JWT |
| useTerminalCmdBar | WS DATA | 经 `sendWsData` | 同上 |
## Closure 表
| H | 文件:行号 | 判定 | 理由 |
|---|-----------|------|------|
| H-Auth | useFilesActions.ts:255-494 | SAFE | 全部经 `@/api` JWT |
| H-Path | useFilesActions.ts:297-301,461-464 | SAFE | `validatePathSegment` 拦截 `/` `..` |
| H-Path | useFilesActions.ts:340-347 | RISK | rename 未调 validatePathSegment;后端 sync_v2 校验 |
| H-Path | useFilesActions.ts:71-78,119 | SAFE | `normalizeRemotePath` / `joinRemotePath` |
| H-Delete | useFilesActions.ts:393-414 | SAFE | confirm + 逐条 delete;失败计数 |
| H-Upload | useFilesActions.ts:486-494 | SAFE | server_id + remote_path 参数化 POST |
| H-Auth | useFilesEditor.ts:94-98 | SAFE | readRemoteFileContent + server_id |
| H-Size | useFilesEditor.ts:63-70 | SAFE | 2MB 编辑上限 `FILE_EDITOR_MAX_BYTES` |
| H-Auth | useFilesPermissions.ts:65-78 | SAFE | JWT GET/POST |
| H-Sudo | useFilesPermissions.ts:73-91 | SAFE | 一键 sudo 走后端 re-auth+SSH;无密码回传 |
| H-Storage | useFilesPermissions.ts:56 | SAFE | sessionStorage 仅 hint dismissed 标记 |
| H-Auth | useTerminalSessions.ts:279-281 | SAFE | webssh-token 绑定 server_id |
| H-WS | useTerminalSessions.ts:293-294 | RISK | query token;单人运维已接受 |
| H-WS | useTerminalSessions.ts:350-363 | SAFE | 4001/4003 分支处理 |
| H-Path | useTerminalSessions.ts:159-163,513,578 | RISK | `sanitizeRemotePath` 未拦 `;|&$`cd 字符串拼接;管理员会话 |
| H-Cmd | useTerminalSessions.ts:648-655 | SAFE | 快捷命令来自 API/内置白名单 |
| H-Cmd | useTerminalCmdBar.ts:21-34 | SAFE | 命令经 WS DATA;后端 dangerous check |
| H-Storage | useTerminalCmdBar.ts:7-33 | RISK | localStorage 存命令历史(非密码);UX 便利 |
| H-Secret | 五文件 | SAFE | 无 password/api_key 存 localStorage |
**len(H)=18, Closure=18**
## FINDING
无 P0/P1 新 FINDING。
可选后续(P2/UX):rename 前端补 `validatePathSegment``sanitizeRemotePath` 收紧或 cd 改用引号包裹。
## DoD
- [x] len(H) == Closure
- [x] 五文件全文 Read(大文件分段至 EOF)
- [x] 无 v-html / 凭据明文存储
## 验证
`bash scripts/local_verify.sh` — 全绿

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