From 9b583ccc9148b1ab962549a5cc440ecb357596c4 Mon Sep 17 00:00:00 2001 From: r Date: Thu, 9 Jul 2026 01:14:18 +0800 Subject: [PATCH] =?UTF-8?q?docs(release):=20=E4=B8=BB=E4=B8=8E=20release?= =?UTF-8?q?=20=E5=88=86=E6=94=AF=E5=90=88=E5=B9=B6=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=B8=85=E5=8D=95=20+=20=E5=AF=BC=E5=85=A5=20release=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 便于 Gitea 上 main ↔ release/20260708-axs 合并评审与冲突对照。 --- .../Codex-审批代理修复说明-20260708.md | 53 + ...us-API服务数据库调用链索引报告-20260707.md | 380 ++++ .../Nexus-API限速与并发限制盘点-20260707.md | 349 +++ ...Nexus-SSH命令执行安全巡检阶段1-20260707.md | 175 ++ ...命令执行安全巡检阶段2-安全解压-20260707.md | 177 ++ ...安全巡检阶段3-文件传输与专用Skill-20260707.md | 87 + .../Nexus-app-v2-双后台实施说明-20260707.md | 1903 +++++++++++++++++ .../Nexus-manual-deploy-commands-20260708.md | 236 ++ ...exus-release-artifact-metadata-20260708.md | 21 + .../Nexus-release-diff-manifest-20260708.md | 777 +++++++ .../20260708/Nexus-release-index-20260708.md | 148 ++ .../Nexus-release-publisher-tests-20260708.md | 55 + ...Nexus-release-python-publisher-20260708.md | 81 + .../Nexus-release-ready-checklist-20260708.md | 495 +++++ ...us-release-text-integrity-gate-20260708.md | 52 + ...exus-代码上下文库与Git基线报告-20260707.md | 174 ++ ...巡检阶段4-settings-auth-btpanel-20260707.md | 368 ++++ ...us-宝塔Session自动修复正式发布-20260707.md | 224 ++ ...-宝塔hardcoded3600批量修复完成-20260707.md | 132 ++ .../Nexus-宝塔一键登录掉线深查-20260707.md | 403 ++++ ...-宝塔一键登录长期掉线修复方案与代码改进.md | 987 +++++++++ .../Nexus-宝塔批量Session共性分析-20260707.md | 91 + .../Nexus-宝塔批量Session巡检-20260707.md | 138 ++ ...宝塔长期掉线修复与测试部署记录-20260707.md | 425 ++++ .../Nexus-新增宝塔Session自动修复-20260707.md | 123 ++ ...新代码上下文索引与Zvec部署报告-20260707.md | 246 +++ .../Nexus-正式环境遗漏补发记录-20260707.md | 109 + ...s-测试部署机环境修复与验证报告-20260707.md | 292 +++ .../20260708/Nexus-项目说明文档-20260708.md | 314 +++ ...PR-MERGE-main与release合并清单-20260709.md | 297 +++ .../20260708/PR-MERGE-说明-20260709.md | 362 ++++ .../20260708/README-Gitea发布说明-20260708.md | 25 + 32 files changed, 9699 insertions(+) create mode 100644 docs/release/20260708/Codex-审批代理修复说明-20260708.md create mode 100644 docs/release/20260708/Nexus-API服务数据库调用链索引报告-20260707.md create mode 100644 docs/release/20260708/Nexus-API限速与并发限制盘点-20260707.md create mode 100644 docs/release/20260708/Nexus-SSH命令执行安全巡检阶段1-20260707.md create mode 100644 docs/release/20260708/Nexus-SSH命令执行安全巡检阶段2-安全解压-20260707.md create mode 100644 docs/release/20260708/Nexus-SSH命令执行安全巡检阶段3-文件传输与专用Skill-20260707.md create mode 100644 docs/release/20260708/Nexus-app-v2-双后台实施说明-20260707.md create mode 100644 docs/release/20260708/Nexus-manual-deploy-commands-20260708.md create mode 100644 docs/release/20260708/Nexus-release-artifact-metadata-20260708.md create mode 100644 docs/release/20260708/Nexus-release-diff-manifest-20260708.md create mode 100644 docs/release/20260708/Nexus-release-index-20260708.md create mode 100644 docs/release/20260708/Nexus-release-publisher-tests-20260708.md create mode 100644 docs/release/20260708/Nexus-release-python-publisher-20260708.md create mode 100644 docs/release/20260708/Nexus-release-ready-checklist-20260708.md create mode 100644 docs/release/20260708/Nexus-release-text-integrity-gate-20260708.md create mode 100644 docs/release/20260708/Nexus-代码上下文库与Git基线报告-20260707.md create mode 100644 docs/release/20260708/Nexus-安全巡检阶段4-settings-auth-btpanel-20260707.md create mode 100644 docs/release/20260708/Nexus-宝塔Session自动修复正式发布-20260707.md create mode 100644 docs/release/20260708/Nexus-宝塔hardcoded3600批量修复完成-20260707.md create mode 100644 docs/release/20260708/Nexus-宝塔一键登录掉线深查-20260707.md create mode 100644 docs/release/20260708/Nexus-宝塔一键登录长期掉线修复方案与代码改进.md create mode 100644 docs/release/20260708/Nexus-宝塔批量Session共性分析-20260707.md create mode 100644 docs/release/20260708/Nexus-宝塔批量Session巡检-20260707.md create mode 100644 docs/release/20260708/Nexus-宝塔长期掉线修复与测试部署记录-20260707.md create mode 100644 docs/release/20260708/Nexus-新增宝塔Session自动修复-20260707.md create mode 100644 docs/release/20260708/Nexus-最新代码上下文索引与Zvec部署报告-20260707.md create mode 100644 docs/release/20260708/Nexus-正式环境遗漏补发记录-20260707.md create mode 100644 docs/release/20260708/Nexus-测试部署机环境修复与验证报告-20260707.md create mode 100644 docs/release/20260708/Nexus-项目说明文档-20260708.md create mode 100644 docs/release/20260708/PR-MERGE-main与release合并清单-20260709.md create mode 100644 docs/release/20260708/PR-MERGE-说明-20260709.md create mode 100644 docs/release/20260708/README-Gitea发布说明-20260708.md diff --git a/docs/release/20260708/Codex-审批代理修复说明-20260708.md b/docs/release/20260708/Codex-审批代理修复说明-20260708.md new file mode 100644 index 00000000..ff0410db --- /dev/null +++ b/docs/release/20260708/Codex-审批代理修复说明-20260708.md @@ -0,0 +1,53 @@ +# Codex 审批代理修复说明(2026-07-08) + +## 问题 + +当前线程的高权限命令审批使用 `auto_review`,它会请求 `codex-auto-review` 模型。 +你的 CC Switch / custom provider 当前只支持 `gpt-5.5`,不支持 `codex-auto-review`,所以 SSH、scp、联网验证都会被拒绝。 + +## 修复方式 + +把当前线程的审批人从: + +```json +"approvalsReviewer": "auto_review" +``` + +改成: + +```json +"approvalsReviewer": "user" +``` + +这样以后高权限命令会弹给你手动确认,而不是走坏掉的自动审核模型。 + +## 我已生成脚本 + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\fix-codex-approval-reviewer.ps1 +``` + +## 运行方法 + +在 PowerShell 里执行: + +```powershell +Set-ExecutionPolicy -Scope Process Bypass +& "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\fix-codex-approval-reviewer.ps1" +``` + +脚本会自动备份: + +```text +C:\Users\uzuma\.codex\.codex-global-state.json.bak-switch-approval-时间戳 +``` + +## 运行后 + +建议重启 Codex,或者关闭再打开当前线程。然后我就可以再次尝试: + +```cmd +python scripts\publish_release_bundle.py --remote nexus --tarball ... --sha256 ... +``` + +届时应该会弹出手动审批,而不是再报 `codex-auto-review` 不支持。 diff --git a/docs/release/20260708/Nexus-API服务数据库调用链索引报告-20260707.md b/docs/release/20260708/Nexus-API服务数据库调用链索引报告-20260707.md new file mode 100644 index 00000000..0fcb8e97 --- /dev/null +++ b/docs/release/20260708/Nexus-API服务数据库调用链索引报告-20260707.md @@ -0,0 +1,380 @@ +# Nexus API / Service / 数据库调用链索引报告 + +生成时间:2026-07-07 + +## 1. 总览 + +| 项目 | 数量 | +| --- | --- | +| API 路由 | 214 | +| Service 方法 | 324 | +| 调用边 | 16780 | +| 数据库调用点 | 437 | +| Redis 调用点 | 729 | +| SSH/远程命令调用点 | 346 | +| 宝塔相关流 | 183 | + +## 2. API 方法分布 + +| HTTP 方法 | 数量 | +| --- | --- | +| POST | 104 | +| GET | 77 | +| PUT | 18 | +| DELETE | 13 | +| PATCH | 2 | + +## 3. API 风险标签分布 + +| 风险标签 | 路由数 | +| --- | --- | +| file | 37 | +| ssh | 35 | +| script | 34 | +| btpanel | 28 | +| install | 18 | +| login | 11 | +| terminal | 7 | + +## 4. 高优先级 API 路由(按风险标签/服务调用筛选) + +| 方法 | 路径 | 风险标签 | 入口 | 函数 | Service 调用候选 | +| --- | --- | --- | --- | --- | --- | +| POST | /script-callback | script | server/api/agent.py:245 | script_job_callback | | +| GET | /nodes | script | server/api/assets.py:126 | list_nodes | | +| GET | /ssh-sessions | script,ssh | server/api/assets.py:230 | list_ssh_sessions | | +| GET | /command-logs | script,ssh | server/api/assets.py:282 | list_command_logs | | +| GET | /login-access | login | server/api/auth.py:116 | login_access | | +| POST | /login | login | server/api/auth.py:128 | login | service.login | +| POST | /refresh | | server/api/auth.py:173 | refresh_token | service.refresh_token | +| POST | /logout | | server/api/auth.py:214 | logout | service.logout_by_token | +| POST | /totp/setup | | server/api/auth.py:235 | setup_totp | service.setup_totp | +| POST | /totp/enable | | server/api/auth.py:258 | enable_totp | service.enable_totp | +| POST | /totp/disable | | server/api/auth.py:280 | disable_totp | service.disable_totp | +| PUT | /password | login | server/api/auth.py:303 | change_password | AuthService, auth_service._delete_all_refresh_tokens, AuthService._verify_totp | +| POST | /webssh-token | ssh | server/api/auth.py:363 | issue_webssh_token | service.create_webssh_token | +| GET | /me | login | server/api/auth.py:385 | get_me | | +| GET | /servers | btpanel | server/api/btpanel.py:60 | list_bt_servers | | +| GET | /servers/{server_id}/config | btpanel,install,script,ssh | server/api/btpanel.py:68 | get_bt_config | | +| PUT | /servers/{server_id}/config | btpanel | server/api/btpanel.py:81 | update_bt_config | | +| GET | /settings | btpanel | server/api/btpanel.py:103 | get_bt_global_settings | | +| PUT | /settings | btpanel | server/api/btpanel.py:111 | update_bt_global_settings | | +| POST | /servers/{server_id}/bootstrap | btpanel | server/api/btpanel.py:129 | bootstrap_bt_server | | +| POST | /servers/batch-bootstrap | btpanel | server/api/btpanel.py:148 | batch_bootstrap_bt_servers | | +| POST | /servers/{server_id}/login-url | btpanel,login | server/api/btpanel.py:166 | create_bt_login_url | | +| GET | /servers/{server_id}/system/total | btpanel | server/api/btpanel.py:183 | bt_system_total | | +| GET | /servers/{server_id}/system/disk | btpanel | server/api/btpanel.py:191 | bt_system_disk | | +| GET | /servers/{server_id}/system/network | btpanel | server/api/btpanel.py:199 | bt_system_network | | +| GET | /servers/{server_id}/sites | btpanel | server/api/btpanel.py:207 | bt_list_sites | | +| POST | /servers/{server_id}/sites/start | btpanel | server/api/btpanel.py:215 | bt_site_start | | +| POST | /servers/{server_id}/sites/stop | btpanel | server/api/btpanel.py:228 | bt_site_stop | | +| GET | /servers/{server_id}/sites/php-versions | btpanel | server/api/btpanel.py:241 | bt_php_versions | | +| POST | /servers/{server_id}/sites | btpanel | server/api/btpanel.py:249 | bt_create_site | | +| GET | /servers/{server_id}/sites/{site_id}/domains | btpanel | server/api/btpanel.py:280 | bt_list_domains | | +| POST | /servers/{server_id}/domains | btpanel | server/api/btpanel.py:293 | bt_add_domain | | +| POST | /servers/{server_id}/domains/delete | btpanel | server/api/btpanel.py:306 | bt_delete_domain | | +| GET | /servers/{server_id}/ssl/sites | btpanel | server/api/btpanel.py:319 | bt_ssl_sites | | +| POST | /servers/{server_id}/ssl/set | btpanel | server/api/btpanel.py:327 | bt_set_ssl | | +| POST | /servers/{server_id}/ssl/apply | btpanel | server/api/btpanel.py:345 | bt_apply_ssl | | +| GET | /servers/{server_id}/databases | btpanel | server/api/btpanel.py:358 | bt_list_databases | | +| POST | /servers/{server_id}/databases | btpanel | server/api/btpanel.py:366 | bt_create_database | | +| POST | /servers/{server_id}/databases/password | btpanel | server/api/btpanel.py:384 | bt_db_password | | +| POST | /servers/{server_id}/databases/backup | btpanel | server/api/btpanel.py:397 | bt_db_backup | | +| GET | /servers/{server_id}/crontab | btpanel | server/api/btpanel.py:410 | bt_crontab | | +| POST | /servers/{server_id}/services | btpanel | server/api/btpanel.py:418 | bt_service_admin | _svc.service_admin | +| GET | '' | script | server/api/execution_records.py:20 | list_records | | +| GET | /{record_id} | script | server/api/execution_records.py:41 | get_record_detail | | +| GET | /browse | file,ssh | server/api/files.py:40 | browse_directory_get | | +| GET | /status | install | server/api/install.py:972 | install_status | | +| GET | /env-check | install | server/api/install.py:1006 | env_check | | +| POST | /test-credentials | install | server/api/install.py:1097 | test_credentials | | +| GET | /connection-check | file,install | server/api/install.py:1113 | connection_check | | +| POST | /init-db | install | server/api/install.py:1153 | init_db | | +| POST | /create-admin | install | server/api/install.py:1327 | create_admin | | +| POST | /lock | install | server/api/install.py:1426 | lock_install | | +| GET | /state | file,install | server/api/install.py:1469 | get_install_state | | +| GET | / | script | server/api/scripts.py:31 | list_scripts | service.list_scripts | +| GET | /exec-config | script | server/api/scripts.py:42 | script_exec_config | | +| GET | /credentials | script | server/api/scripts.py:56 | list_credentials | service.list_credentials | +| POST | /credentials | script | server/api/scripts.py:66 | create_credential | service.create_credential | +| PUT | /credentials/{id} | script | server/api/scripts.py:90 | update_credential | | +| DELETE | /credentials/{id} | script | server/api/scripts.py:131 | delete_credential | service.delete_credential | +| GET | /executions | script | server/api/scripts.py:152 | list_executions | service.list_executions | +| POST | /executions/{id}/stop | script | server/api/scripts.py:167 | stop_execution | service.stop_execution, service.get_execution_detail | +| POST | /executions/{id}/mark-stuck | script | server/api/scripts.py:181 | mark_execution_stuck | service.mark_execution_stuck, service.get_execution_detail | +| POST | /executions/{id}/retry | script | server/api/scripts.py:198 | retry_execution | service._parse_execution_meta, service.get_execution, service.get_execution_detail, service.retry_execution | +| GET | /executions/{id} | script | server/api/scripts.py:238 | get_execution | service.get_execution_detail | +| GET | /{id} | script | server/api/scripts.py:258 | get_script | service.get_script | +| POST | / | script | server/api/scripts.py:271 | create_script | service.create_script | +| PUT | /{id} | script | server/api/scripts.py:295 | update_script | service.get_script, service.update_script | +| DELETE | /{id} | script | server/api/scripts.py:325 | delete_script | service.get_script, service.delete_script | +| POST | /exec | script | server/api/scripts.py:364 | execute_command | service.get_execution_detail, service.execute_command | +| GET | / | script | server/api/search.py:32 | global_search | | +| GET | / | script | server/api/servers.py:94 | list_servers | | +| GET | /terminal-search-history | terminal | server/api/servers.py:436 | get_terminal_search_history | | +| PUT | /terminal-search-history | terminal | server/api/servers.py:454 | update_terminal_search_history | | +| GET | /logs | script | server/api/servers.py:491 | get_all_sync_logs | | +| GET | /meta/api_base_url | install | server/api/servers.py:561 | get_api_base_url | | +| GET | /import/template | file | server/api/servers.py:570 | download_import_template | | +| POST | /import | file | server/api/servers.py:589 | import_servers | service.create_server | +| POST | /batch/install-agent | install | server/api/servers.py:832 | batch_install_agent | | +| POST | /batch/detect-path | ssh | server/api/servers.py:850 | batch_detect_path | | +| POST | /batch/uninstall-agent | install | server/api/servers.py:859 | batch_uninstall_agent | | +| GET | /pending | script,ssh | server/api/servers.py:977 | list_pending_servers | | +| POST | /add-by-ip | login,ssh | server/api/servers.py:1099 | add_server_by_ip | | +| GET | /{id} | | server/api/servers.py:1334 | get_server | service.get_server | +| GET | /{id}/metrics | | server/api/servers.py:1362 | get_server_metrics | service.get_server | +| GET | /{id}/files-capability | file,ssh | server/api/servers.py:1407 | get_files_capability | service.get_server | +| POST | /{id}/setup-files-sudo | file,install,ssh | server/api/servers.py:1422 | setup_files_sudo | service.get_server | +| POST | / | file,ssh | server/api/servers.py:1463 | create_server | service.create_server | +| PUT | /{id} | file,ssh | server/api/servers.py:1537 | update_server | service.get_server, service.update_server | +| DELETE | /{id} | | server/api/servers.py:1629 | delete_server | service.get_server, service.delete_server | +| POST | /{id}/agent-key | | server/api/servers.py:1659 | generate_agent_api_key | service.get_server, service.update_server | +| POST | /{id}/install-agent | install,ssh | server/api/servers.py:1711 | install_agent_remote | service.get_server, service.update_server | +| POST | /{id}/uninstall-agent | file,install,ssh | server/api/servers.py:1818 | uninstall_agent_remote | service.get_server | +| POST | /{id}/agent-install-cmd | install | server/api/servers.py:1897 | get_agent_install_cmd | service.get_server | +| POST | /{id}/upgrade-agent | ssh | server/api/servers.py:1965 | upgrade_agent | service.get_server | +| POST | /{id}/agent-diagnose | ssh | server/api/servers.py:2067 | agent_diagnose | service.get_server | +| POST | /check | ssh | server/api/servers.py:2105 | check_servers | | +| GET | /ip-allowlist | login,script | server/api/settings.py:168 | get_ip_allowlist | | +| GET | /bing-wallpaper | file | server/api/settings.py:307 | bing_wallpaper | | +| PUT | /{id} | script | server/api/settings.py:476 | update_schedule | | +| GET | / | ssh | server/api/settings.py:684 | list_ssh_key_presets | | +| POST | / | ssh | server/api/settings.py:702 | create_ssh_key_preset | | +| PUT | /{id} | ssh | server/api/settings.py:742 | update_ssh_key_preset | | +| POST | /{id}/reveal | ssh | server/api/settings.py:795 | reveal_ssh_key_preset | | +| DELETE | /{id} | ssh | server/api/settings.py:828 | delete_ssh_key_preset | | +| GET | / | script | server/api/settings.py:856 | list_alert_history | | +| GET | /ops-patrol/status | install | server/api/settings.py:1035 | ops_patrol_status | | +| POST | /ops-patrol/sync-telegram | file | server/api/settings.py:1056 | ops_patrol_sync_telegram | | +| POST | /ip-allowlist/parse-subscription | script | server/api/settings.py:1398 | parse_subscription | | +| POST | /ip-allowlist | login,script | server/api/settings.py:1503 | set_ip_allowlist | | +| POST | /ip-allowlist/toggle | login | server/api/settings.py:1569 | toggle_ip_allowlist | | +| POST | /ip-allowlist/manual | login | server/api/settings.py:1596 | add_manual_ips | | +| DELETE | /ip-allowlist/ip | login | server/api/settings.py:1632 | remove_allowlist_ip | | +| POST | /files | file | server/api/sync_v2.py:80 | sync_files | | +| POST | /preview | file | server/api/sync_v2.py:128 | preview_sync | | +| POST | /file-ops | file,ssh | server/api/sync_v2.py:161 | file_operation | | +| POST | /file-clipboard | file,ssh | server/api/sync_v2.py:228 | apply_file_clipboard | | +| POST | /browse | file,ssh | server/api/sync_v2.py:291 | browse_directory | | +| POST | /browse-local | file | server/api/sync_v2.py:322 | browse_local_directory | | +| POST | /local-file-ops | file | server/api/sync_v2.py:361 | local_file_operation | | +| POST | /local-file-preview | file | server/api/sync_v2.py:432 | local_file_preview | | + +## 5. 典型调用链样例 + +| API | 入口 | 标签 | 调用链候选 | +| --- | --- | --- | --- | +| POST /script-callback | server/api/agent.py:245 | script | 未匹配到 service_calls | +| GET /nodes | server/api/assets.py:126 | script | 未匹配到 service_calls | +| GET /ssh-sessions | server/api/assets.py:230 | script,ssh | 未匹配到 service_calls | +| GET /command-logs | server/api/assets.py:282 | script,ssh | 未匹配到 service_calls | +| GET /login-access | server/api/auth.py:116 | login | 未匹配到 service_calls | +| POST /login | server/api/auth.py:128 | login | AuthService.login(DB×7;Redis×15;SSH×2) | +| PUT /password | server/api/auth.py:303 | login | AuthService.__init__(DB×7;Redis×15;SSH×2) -> AuthService.refresh_token(DB×7;Redis×15;SSH×2) -> AuthService._verify_totp(DB×7;Redis×15;SSH×2) | +| POST /webssh-token | server/api/auth.py:363 | ssh | AuthService.create_webssh_token(DB×7;Redis×15;SSH×2) | +| GET /me | server/api/auth.py:385 | login | 未匹配到 service_calls | +| GET /servers | server/api/btpanel.py:60 | btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/config | server/api/btpanel.py:68 | script,ssh,btpanel,install | 未匹配到 service_calls | +| PUT /servers/{server_id}/config | server/api/btpanel.py:81 | btpanel | 未匹配到 service_calls | +| GET /settings | server/api/btpanel.py:103 | btpanel | 未匹配到 service_calls | +| PUT /settings | server/api/btpanel.py:111 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/bootstrap | server/api/btpanel.py:129 | btpanel | 未匹配到 service_calls | +| POST /servers/batch-bootstrap | server/api/btpanel.py:148 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/login-url | server/api/btpanel.py:166 | login,btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/system/total | server/api/btpanel.py:183 | btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/system/disk | server/api/btpanel.py:191 | btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/system/network | server/api/btpanel.py:199 | btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/sites | server/api/btpanel.py:207 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/sites/start | server/api/btpanel.py:215 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/sites/stop | server/api/btpanel.py:228 | btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/sites/php-versions | server/api/btpanel.py:241 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/sites | server/api/btpanel.py:249 | btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/sites/{site_id}/domains | server/api/btpanel.py:280 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/domains | server/api/btpanel.py:293 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/domains/delete | server/api/btpanel.py:306 | btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/ssl/sites | server/api/btpanel.py:319 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/ssl/set | server/api/btpanel.py:327 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/ssl/apply | server/api/btpanel.py:345 | btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/databases | server/api/btpanel.py:358 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/databases | server/api/btpanel.py:366 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/databases/password | server/api/btpanel.py:384 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/databases/backup | server/api/btpanel.py:397 | btpanel | 未匹配到 service_calls | +| GET /servers/{server_id}/crontab | server/api/btpanel.py:410 | btpanel | 未匹配到 service_calls | +| POST /servers/{server_id}/services | server/api/btpanel.py:418 | btpanel | BtPanelService.service_admin(DB×7;Redis×16;SSH×7) | +| GET '' | server/api/execution_records.py:20 | script | 未匹配到 service_calls | +| GET /{record_id} | server/api/execution_records.py:41 | script | 未匹配到 service_calls | +| GET /browse | server/api/files.py:40 | file,ssh | 未匹配到 service_calls | +| GET /connection-check | server/api/install.py:1113 | file,install | 未匹配到 service_calls | +| GET /state | server/api/install.py:1469 | file,install | 未匹配到 service_calls | +| GET / | server/api/scripts.py:31 | script | ScriptService.list_scripts(DB×9;Redis×13;SSH×9) | +| GET /exec-config | server/api/scripts.py:42 | script | 未匹配到 service_calls | +| GET /credentials | server/api/scripts.py:56 | script | ScriptService.list_credentials(DB×9;Redis×13;SSH×9) | +| POST /credentials | server/api/scripts.py:66 | script | ScriptService.create_credential(DB×9;Redis×13;SSH×9) | +| PUT /credentials/{id} | server/api/scripts.py:90 | script | 未匹配到 service_calls | +| DELETE /credentials/{id} | server/api/scripts.py:131 | script | ScriptService.delete_credential(DB×9;Redis×13;SSH×9) | +| GET /executions | server/api/scripts.py:152 | script | ScriptService.list_executions(DB×9;Redis×13;SSH×9) | +| POST /executions/{id}/stop | server/api/scripts.py:167 | script | ScriptService.stop_execution(DB×9;Redis×13;SSH×9) -> ScriptService.get_execution(DB×9;Redis×13;SSH×9) | +| POST /executions/{id}/mark-stuck | server/api/scripts.py:181 | script | ScriptService.mark_execution_stuck(DB×9;Redis×13;SSH×9) -> ScriptService.get_execution(DB×9;Redis×13;SSH×9) | +| POST /executions/{id}/retry | server/api/scripts.py:198 | script | ScriptService._parse_execution_meta(DB×9;Redis×13;SSH×9) -> get_execution_record_detail(Redis×6) -> ScriptService.get_execution(DB×9;Redis×13;SSH×9) -> ScriptService.retry_executio... | +| GET /executions/{id} | server/api/scripts.py:238 | script | ScriptService.get_execution(DB×9;Redis×13;SSH×9) | +| GET /{id} | server/api/scripts.py:258 | script | ScriptService.get_script(DB×9;Redis×13;SSH×9) | +| POST / | server/api/scripts.py:271 | script | ScriptService.create_script(DB×9;Redis×13;SSH×9) | +| PUT /{id} | server/api/scripts.py:295 | script | ScriptService.get_script(DB×9;Redis×13;SSH×9) -> ScriptService.update_script(DB×9;Redis×13;SSH×9) | +| DELETE /{id} | server/api/scripts.py:325 | script | ScriptService.get_script(DB×9;Redis×13;SSH×9) -> ScriptService.delete_script(DB×9;Redis×13;SSH×9) | +| POST /exec | server/api/scripts.py:364 | script | ScriptService.get_execution(DB×9;Redis×13;SSH×9) -> ScriptService.execute_command(DB×9;Redis×13;SSH×9) | +| GET / | server/api/search.py:32 | script | 未匹配到 service_calls | +| GET / | server/api/servers.py:94 | script | 未匹配到 service_calls | +| GET /terminal-search-history | server/api/servers.py:436 | terminal | 未匹配到 service_calls | +| PUT /terminal-search-history | server/api/servers.py:454 | terminal | 未匹配到 service_calls | +| GET /logs | server/api/servers.py:491 | script | 未匹配到 service_calls | +| GET /import/template | server/api/servers.py:570 | file | 未匹配到 service_calls | +| POST /import | server/api/servers.py:589 | file | ServerService.create_server(DB×2;Redis×3;SSH×2) | +| POST /batch/detect-path | server/api/servers.py:850 | ssh | 未匹配到 service_calls | +| GET /pending | server/api/servers.py:977 | script,ssh | 未匹配到 service_calls | +| POST /add-by-ip | server/api/servers.py:1099 | login,ssh | 未匹配到 service_calls | +| GET /{id}/files-capability | server/api/servers.py:1407 | file,ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) | +| POST /{id}/setup-files-sudo | server/api/servers.py:1422 | file,ssh,install | BtPanelService.get_server(DB×7;Redis×16;SSH×7) | +| POST / | server/api/servers.py:1463 | file,ssh | ServerService.create_server(DB×2;Redis×3;SSH×2) | +| PUT /{id} | server/api/servers.py:1537 | file,ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) -> ServerService.update_server(DB×2;Redis×3;SSH×2) | +| POST /{id}/install-agent | server/api/servers.py:1711 | ssh,install | BtPanelService.get_server(DB×7;Redis×16;SSH×7) -> ServerService.update_server(DB×2;Redis×3;SSH×2) | +| POST /{id}/uninstall-agent | server/api/servers.py:1818 | file,ssh,install | BtPanelService.get_server(DB×7;Redis×16;SSH×7) | +| POST /{id}/upgrade-agent | server/api/servers.py:1965 | ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) | +| POST /{id}/agent-diagnose | server/api/servers.py:2067 | ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) | +| POST /check | server/api/servers.py:2105 | ssh | 未匹配到 service_calls | +| GET /ip-allowlist | server/api/settings.py:168 | login,script | 未匹配到 service_calls | +| GET /bing-wallpaper | server/api/settings.py:307 | file | 未匹配到 service_calls | +| PUT /{id} | server/api/settings.py:476 | script | 未匹配到 service_calls | + +## 6. 宝塔相关流 + +| 类型 | 位置 | 函数 | 标签/Key | 摘要 | +| --- | --- | --- | --- | --- | +| api | server/api/btpanel.py:60 | list_bt_servers | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_server_statuses', '_svc'], 'fi | +| api | server/api/btpanel.py:68 | get_bt_config | btpanel,install,script,ssh | {'auth_required_inferred': True, 'calls': ['router.get', 'Query', 'Depends', 'Depends', '_svc.get_config', '_http_error' | +| api | server/api/btpanel.py:81 | update_bt_config | btpanel | {'auth_required_inferred': True, 'calls': ['router.put', 'Depends', 'Depends', '_svc.update_config', '_http_error', '_sv | +| api | server/api/btpanel.py:103 | get_bt_global_settings | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.get_global_settings', '_svc'], 'fil | +| api | server/api/btpanel.py:111 | update_bt_global_settings | btpanel | {'auth_required_inferred': True, 'calls': ['router.put', 'Depends', 'Depends', '_svc.update_global_settings', '_http_err | +| api | server/api/btpanel.py:129 | bootstrap_bt_server | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.bootstrap_server', '_http_error', | +| api | server/api/btpanel.py:148 | batch_bootstrap_bt_servers | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.batch_bootstrap', '_http_error', ' | +| api | server/api/btpanel.py:166 | create_bt_login_url | btpanel,login | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.create_login_url', '_http_error', | +| api | server/api/btpanel.py:183 | bt_system_total | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.system_total', '_http_error', '_svc | +| api | server/api/btpanel.py:191 | bt_system_disk | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.system_disk', '_http_error', '_svc' | +| api | server/api/btpanel.py:199 | bt_system_network | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.system_network', '_http_error', '_s | +| api | server/api/btpanel.py:207 | bt_list_sites | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_sites', '_http_error', '_svc'] | +| api | server/api/btpanel.py:215 | bt_site_start | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.site_start', '_http_error', '_svc' | +| api | server/api/btpanel.py:228 | bt_site_stop | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.site_stop', '_http_error', '_svc'] | +| api | server/api/btpanel.py:241 | bt_php_versions | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.php_versions', '_http_error', '_svc | +| api | server/api/btpanel.py:249 | bt_create_site | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', 'json.dumps', '_svc.create_site', '_http | +| api | server/api/btpanel.py:280 | bt_list_domains | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_domains', '_http_error', '_svc | +| api | server/api/btpanel.py:293 | bt_add_domain | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.add_domain', '_http_error', 'body. | +| api | server/api/btpanel.py:306 | bt_delete_domain | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.delete_domain', '_http_error', 'bo | +| api | server/api/btpanel.py:319 | bt_ssl_sites | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_ssl_sites', '_http_error', '_s | +| api | server/api/btpanel.py:327 | bt_set_ssl | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.set_ssl', '_http_error', 'body.mod | +| api | server/api/btpanel.py:345 | bt_apply_ssl | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.apply_letsencrypt', '_http_error', | +| api | server/api/btpanel.py:358 | bt_list_databases | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_databases', '_http_error', '_s | +| api | server/api/btpanel.py:366 | bt_create_database | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.create_database', '_http_error', ' | +| api | server/api/btpanel.py:384 | bt_db_password | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.reset_database_password', '_http_e | +| api | server/api/btpanel.py:397 | bt_db_backup | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.backup_database', '_http_error', ' | +| api | server/api/btpanel.py:410 | bt_crontab | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_crontab', '_http_error', '_svc | +| api | server/api/btpanel.py:418 | bt_service_admin | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.service_admin', '_http_error', '_s | +| service | server/application/services/btpanel_bootstrap_schedule.py:24 | | btpanel | {'calls': ["getattr(settings, 'BT_PANEL_AUTO_BOOTSTRAP_ENABLED', None) or 'true'.strip.lower", "getattr(settings, 'BT_PA | +| service | server/application/services/btpanel_bootstrap_schedule.py:29 | | btpanel | {'calls': ['getattr', 'max', 'int', 'min'], 'class': None, 'end_line': 35, 'file': 'server/application/services/btpanel_ | +| service | server/application/services/btpanel_bootstrap_schedule.py:38 | | btpanel,session | {'calls': ['int', 'AsyncSessionLocal', 'ServerRepositoryImpl', 'mark_bootstrap_pending', 'repo.get_by_id', 'bt_panel_con | +| service | server/application/services/btpanel_bootstrap_schedule.py:54 | | btpanel | {'calls': ['is_auto_bootstrap_enabled', 'int', 'mark_servers_bootstrap_pending', 'asyncio.create_task', '_BG_TASKS.add', | +| service | server/application/services/btpanel_bootstrap_schedule.py:68 | | btpanel,session | {'calls': ['AsyncSessionLocal', 'BtPanelService', 'logger.warning', 'svc.bootstrap_server'], 'class': None, 'end_line': | +| service | server/application/services/btpanel_bootstrap_schedule.py:84 | | btpanel,session | {'calls': ['datetime.now', 'bootstrap_batch_size', 'is_auto_bootstrap_enabled', 'AsyncSessionLocal', 'ServerRepositoryIm | +| service | server/application/services/btpanel_service.py:54 | | btpanel | {'calls': ['isinstance'], 'class': None, 'end_line': 55, 'file': 'server/application/services/btpanel_service.py', 'id': | +| service | server/application/services/btpanel_service.py:59 | | btpanel | {'calls': ['ServerRepositoryImpl', 'AuditLogRepositoryImpl'], 'class': 'BtPanelService', 'end_line': 62, 'file': 'server | +| service | server/application/services/btpanel_service.py:64 | | btpanel | {'calls': ['self.servers.get_by_id', 'ValueError'], 'class': 'BtPanelService', 'end_line': 68, 'file': 'server/applicati | +| service | server/application/services/btpanel_service.py:70 | | btpanel | {'calls': ['read_bt_panel_credentials', 'BtPanelClient', 'ValueError'], 'class': 'BtPanelService', 'end_line': 74, 'file | +| service | server/application/services/btpanel_service.py:76 | | btpanel,redis | {'calls': ['get_redis', 'redis.pipeline', 'zip', 'self.servers.get_all', 'pipe.hgetall', 'pipe.execute', 'agent_is_insta | +| service | server/application/services/btpanel_service.py:112 | | btpanel | {'calls': ['self.get_server', 'self._resolve_bt_installed', 'public_bt_panel_status'], 'class': 'BtPanelService', 'end_l | +| service | server/application/services/btpanel_service.py:117 | | btpanel | {'calls': ['get_bt_panel_block', 'block.get', 'block.get', 'merge_bootstrap_fields', 'bt_panel_configured', 'detect_bt_p | +| service | server/application/services/btpanel_service.py:141 | | btpanel | {'calls': ["getattr(settings, 'BT_PANEL_SOURCE_IP', None) or ''.strip", 'self._auto_bootstrap_enabled', 'get_bt_panel_so | +| service | server/application/services/btpanel_service.py:150 | | btpanel | {'calls': ['bt_panel_source_ip.strip', 'self.audit.create', 'self.get_global_settings', 'ValueError', 'self._persist_set | +| service | server/application/services/btpanel_service.py:183 | | btpanel | {'calls': ['SettingRepositoryImpl', 'settings.apply_db_override', 'repo.set', 'publish_setting_change'], 'class': 'BtPan | +| service | server/application/services/btpanel_service.py:194 | | btpanel | {'calls': ['is_auto_bootstrap_enabled'], 'class': 'BtPanelService', 'end_line': 197, 'file': 'server/application/service | +| service | server/application/services/btpanel_service.py:199 | | btpanel | {'calls': ['merge_bt_panel_extra', 'self.get_server', 'self.servers.update', 'self.audit.create', 'self.get_config', 'is | +| service | server/application/services/btpanel_service.py:249 | | btpanel | {'calls': ['bootstrap_lock', 'self._bootstrap_server_locked'], 'class': 'BtPanelService', 'end_line': 265, 'file': 'serv | +| service | server/application/services/btpanel_service.py:267 | | btpanel | {'calls': ['get_bt_panel_source_ip', 'str.strip', 'str.strip', 'apply_bootstrap_success', 'self.get_server', 'bt_panel_c | +| service | server/application/services/btpanel_service.py:368 | | btpanel | {'calls': ['ValueError', 'start_batch_job', 'self.list_server_statuses', 'int', 'int', 'ids.append', 's.get'], 'class': | +| service | server/application/services/btpanel_service.py:398 | | btpanel | {'calls': ['self.audit.create', 'AuditLog', 'json.dumps'], 'class': 'BtPanelService', 'end_line': 421, 'file': 'server/a | +| service | server/application/services/btpanel_service.py:424 | | btpanel | {'calls': ['getattr', 'getattr', 'bool', "getattr(server, 'auth_method', None) or ''.strip", 'bool', 'getattr', 'getattr | +| service | server/application/services/btpanel_service.py:437 | | btpanel,session | {'calls': ['get_bt_panel_block', '_parse_iso', '_utcnow() - checked_at.total_seconds', 'block.get', 'block.get', '_utcno | +| service | server/application/services/btpanel_service.py:447 | | btpanel,session | {'calls': ['merge_bootstrap_fields', 'self.servers.update', 'self.get_server', 'isinstance', '_iso', '_utcnow'], 'class' | +| service | server/application/services/btpanel_service.py:457 | | btpanel,session | {'calls': ['get_bt_panel_source_ip', 'str.strip', 'str.strip', 'apply_bootstrap_success', 'self._session_cleanup_ttl_che | +| service | server/application/services/btpanel_service.py:502 | | btpanel | {'calls': ['login_url_lock', 'self._create_login_url_fresh', 'self._audit_login_url_failure'], 'class': 'BtPanelService' | +| service | server/application/services/btpanel_service.py:529 | | btpanel,session | {'calls': ['bt_panel_configured', 'read_bt_panel_credentials', 'self.get_server', 'bt_panel_configured', 'read_bt_panel_ | +| service | server/application/services/btpanel_service.py:585 | | btpanel | {'calls': ['self.audit.create', 'logger.warning', 'AuditLog', 'json.dumps'], 'class': 'BtPanelService', 'end_line': 607, | +| service | server/application/services/btpanel_service.py:609 | | btpanel | {'calls': ['self._repair_ip_whitelist', 'read_bt_panel_credentials', 'logger.warning', 'logger.warning', 'self.get_serve | +| service | server/application/services/btpanel_service.py:642 | | btpanel | {'calls': ['BtPanelClient', 'isinstance', 'BtPanelApiError', 'int', 'client.post', 'data.get', 'time.time', 'data.get', | +| service | server/application/services/btpanel_service.py:674 | | btpanel | {'calls': ['self._client', 'self.get_server', 'client.post', 'str', 'self.get_server', 'self._client.post', 'self._repai | +| service | server/application/services/btpanel_service.py:692 | | btpanel | {'calls': ['get_bt_panel_source_ip', 'str.strip', 'str.strip', 'logger.info', 'logger.warning', 'apply_bootstrap_success | +| service | server/application/services/btpanel_service.py:722 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 723, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:725 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 726, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:728 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 729, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:732 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 738, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:740 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 741, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:743 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 744, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:747 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 748, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:750 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps', 'payload.get'], 'class': 'BtPanelService', 'end_ | +| service | server/application/services/btpanel_service.py:763 | | btpanel | {'calls': ['self.proxy', 'str'], 'class': 'BtPanelService', 'end_line': 768, 'file': 'server/application/services/btpane | +| service | server/application/services/btpanel_service.py:770 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 771, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:773 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 774, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:777 | | btpanel | {'calls': ['self.list_sites'], 'class': 'BtPanelService', 'end_line': 778, 'file': 'server/application/services/btpanel_ | +| service | server/application/services/btpanel_service.py:780 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps', 'payload.get'], 'class': 'BtPanelService', 'end_ | +| service | server/application/services/btpanel_service.py:792 | | btpanel,crypto | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 793, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:796 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 801, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:803 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps', 'payload.get'], 'class': 'BtPanelService', 'end_ | +| service | server/application/services/btpanel_service.py:815 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 816, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:818 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 819, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:822 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 823, 'file': 'server/application/services/btpanel_servi | +| service | server/application/services/btpanel_service.py:826 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps'], 'class': 'BtPanelService', 'end_line': 836, 'fi | +| service | server/application/services/server_batch_service.py:774 | | btpanel,session | {'calls': ['str', 'server_map.get', '_run_with_semaphore', 'live.get', 'result_item_dict', 'result.get', 'result.get', ' | +| service | server/application/services/server_batch_service.py:779 | | btpanel,session | {'calls': ['server_map.get', 'result_item_dict', 'result.get', 'result.get', 'result_item_dict', 'AsyncSessionLocal', 'B | +| service | server/application/services/server_file_transfer_service.py:397 | | btpanel,token | {'calls': ['read_bt_panel_credentials', 'signed_download_url', 'ServerTransferError', 'ServerTransferError', 'normalize_ | +| service | server/application/services/server_file_transfer_service.py:497 | | btpanel | {'calls': ['_assert_distinct_servers', 'int', '_get_server', 'bt_panel_configured', 'read_bt_panel_credentials', 'create | +| redis | tests/test_btpanel_client.py:39 | test_merge_bt_panel_extra_encrypts_key | enc:secret,enc:{s} | {'file': 'tests/test_btpanel_client.py', 'function': 'test_merge_bt_panel_extra_encrypts_key', 'id': 'tests/test_btpanel | +| redis | tests/test_btpanel_get_config.py:64 | test_bootstrap_lock_serializes_same_server | | {'file': 'tests/test_btpanel_get_config.py', 'function': 'test_bootstrap_lock_serializes_same_server', 'id': 'tests/test | +| redis | tests/test_btpanel_get_config.py:67 | test_bootstrap_lock_serializes_same_server.worker | | {'file': 'tests/test_btpanel_get_config.py', 'function': 'test_bootstrap_lock_serializes_same_server.worker', 'id': 'tes | +| redis | tests/test_btpanel_login_url.py:19 | _FakeRedis.get | | {'file': 'tests/test_btpanel_login_url.py', 'function': '_FakeRedis.get', 'id': 'tests/test_btpanel_login_url.py::_FakeR | +| redis | tests/test_btpanel_login_url.py:27 | _noop_login_url_lock | | {'file': 'tests/test_btpanel_login_url.py', 'function': '_noop_login_url_lock', 'id': 'tests/test_btpanel_login_url.py:: | +| redis | tests/test_btpanel_login_url.py:32 | test_create_login_url_bootstraps_when_unconfigured | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_bootstraps_when_unconfigured', 'id': 'tes | +| redis | tests/test_btpanel_login_url.py:78 | test_create_login_url_does_not_reuse_single_use_tokens | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_does_not_reuse_single_use_tokens', 'id': | +| redis | tests/test_btpanel_login_url.py:119 | test_create_login_url_skips_bootstrap_when_configured | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_skips_bootstrap_when_configured', 'id': ' | +| redis | tests/test_btpanel_login_url.py:161 | test_create_login_url_ensures_session_cleanup_ttl_once_for_configured_server | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_ensures_session_cleanup_ttl_once_for_conf | +| redis | tests/test_btpanel_login_url.py:224 | test_create_login_url_repairs_stale_port_then_uses_api | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_repairs_stale_port_then_uses_api', 'id': | +| redis | tests/test_btpanel_login_url.py:288 | test_create_login_url_falls_back_to_ssh_when_api_unreachable | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_falls_back_to_ssh_when_api_unreachable', | +| redis | tests/test_btpanel_server_list.py:23 | test_list_server_statuses_includes_live_online_from_redis | | {'file': 'tests/test_btpanel_server_list.py', 'function': 'test_list_server_statuses_includes_live_online_from_redis', ' | +| redis | tests/test_btpanel_ssh_bootstrap.py:34 | test_mark_bootstrap_pending_sets_now | | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_mark_bootstrap_pending_sets_now', 'id': 'tests/test_bt | +| redis | tests/test_btpanel_ssh_bootstrap.py:42 | test_apply_failure_bt_not_installed_goes_installing | | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_apply_failure_bt_not_installed_goes_installing', 'id': | +| redis | tests/test_btpanel_ssh_bootstrap.py:50 | test_apply_failure_ssh_auth_is_permanent | | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_apply_failure_ssh_auth_is_permanent', 'id': 'tests/tes | +| redis | tests/test_btpanel_ssh_bootstrap.py:57 | test_apply_success_sets_ready | enc:token123,enc:{s} | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_apply_success_sets_ready', 'id': 'tests/test_btpanel_s | +| redis | server/api/btpanel.py:46 | _http_error | | {'file': 'server/api/btpanel.py', 'function': '_http_error', 'id': 'server/api/btpanel.py::_http_error::redis', 'key_pat | +| redis | server/infrastructure/btpanel/bootstrap_lock.py:13 | bootstrap_lock | | {'file': 'server/infrastructure/btpanel/bootstrap_lock.py', 'function': 'bootstrap_lock', 'id': 'server/infrastructure/b | +| redis | server/infrastructure/btpanel/bootstrap_state.py:43 | get_bootstrap_block | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'get_bootstrap_block', 'id': 'server/infrastruc | +| redis | server/infrastructure/btpanel/bootstrap_state.py:78 | public_bootstrap_fields | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'public_bootstrap_fields', 'id': 'server/infras | +| redis | server/infrastructure/btpanel/bootstrap_state.py:96 | merge_bootstrap_fields | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'merge_bootstrap_fields', 'id': 'server/infrast | +| redis | server/infrastructure/btpanel/bootstrap_state.py:126 | bt_panel_configured_from_attrs | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'bt_panel_configured_from_attrs', 'id': 'server | +| redis | server/infrastructure/btpanel/bootstrap_state.py:135 | is_eligible_for_background_bootstrap | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'is_eligible_for_background_bootstrap', 'id': ' | +| redis | server/infrastructure/btpanel/bootstrap_state.py:155 | apply_bootstrap_success | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'apply_bootstrap_success', 'id': 'server/infras | +| redis | server/infrastructure/btpanel/bootstrap_state.py:196 | apply_bootstrap_failure | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'apply_bootstrap_failure', 'id': 'server/infras | +| redis | server/infrastructure/btpanel/bootstrap_state.py:223 | get_bootstrap_block_from_attrs | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'get_bootstrap_block_from_attrs', 'id': 'server | +| redis | server/infrastructure/btpanel/client.py:29 | raise_if_panel_error | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'raise_if_panel_error', 'id': 'server/infrastructure/btp | +| redis | server/infrastructure/btpanel/client.py:50 | BtPanelClient._load_cookies | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'BtPanelClient._load_cookies', 'id': 'server/infrastruct | +| redis | server/infrastructure/btpanel/client.py:60 | BtPanelClient._save_cookies | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'BtPanelClient._save_cookies', 'id': 'server/infrastruct | +| redis | server/infrastructure/btpanel/client.py:64 | BtPanelClient.post | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'BtPanelClient.post', 'id': 'server/infrastructure/btpan | +| redis | server/infrastructure/btpanel/credentials.py:26 | get_bt_panel_block | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'get_bt_panel_block', 'id': 'server/infrastructure/ | +| redis | server/infrastructure/btpanel/credentials.py:31 | bt_panel_configured | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'bt_panel_configured', 'id': 'server/infrastructure | +| redis | server/infrastructure/btpanel/credentials.py:36 | get_bt_panel_base_url | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'get_bt_panel_base_url', 'id': 'server/infrastructu | +| redis | server/infrastructure/btpanel/credentials.py:41 | read_bt_panel_credentials | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'read_bt_panel_credentials', 'id': 'server/infrastr | +| redis | server/infrastructure/btpanel/credentials.py:54 | merge_bt_panel_extra | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'merge_bt_panel_extra', 'id': 'server/infrastructur | + +## 7. 安全审查优先级建议 + +1. **宝塔一键登录/session**:优先审查 `server/application/services/btpanel_service.py`、`server/infrastructure/btpanel/*`,关注 token TTL、Redis lock、并发打开 10 个面板、失败回退和审计日志。 +2. **文件管理/归档/解压**:优先审查带 `file` 标签的 API 与 `server/infrastructure/ssh/remote_archive.py`、文件传输路径,关注路径穿越、tar/zip 选项注入、软链、绝对路径。 +3. **SSH/远程 shell**:用户确认管理员远程执行 shell 是业务能力,不作为漏洞本身;审查重点应放在鉴权、审计、超时、输出截断、日志脱敏、命令模板边界。 +4. **数据库访问**:对 `db_queries.jsonl` 中写操作链路检查权限过滤、事务 commit/rollback、分页上限和多租户隔离。 +5. **Redis 锁/缓存**:对 login/session/任务类 Redis key 检查 TTL、锁释放、失败重试与缓存穿透。 diff --git a/docs/release/20260708/Nexus-API限速与并发限制盘点-20260707.md b/docs/release/20260708/Nexus-API限速与并发限制盘点-20260707.md new file mode 100644 index 00000000..8fb08cd5 --- /dev/null +++ b/docs/release/20260708/Nexus-API限速与并发限制盘点-20260707.md @@ -0,0 +1,349 @@ +# Nexus API 限速与并发限制盘点 + +日期:2026-07-07 +范围:本地代码上下文快照 `work/nexus_code_context/snapshots/nexus-source-20260707-112042`。 +说明:本报告只盘点代码内的 API rate limit、429、throttle、并发/批量限制;不包含外部平台(例如 Codex 模型 API)的 429。 + +## 结论 + +1. 当前 Nexus 后端没有发现全局 HTTP API 限速中间件。 + - 未发现 slowapi / fastapi-limiter / starlette limiter 等依赖或全局 middleware。 + - `server/main.py` 主要是安装模式、DB session、安全头、AppAuth、JWT、CORS 等中间件,没有全局 rate limit。 + +2. 当前会返回 HTTP 429 的后端代码只有两类: + - 管理员登录失败锁定:`/api/auth/login`。 + - 子机脚本回调限速:`/api/agent/script-callback`。 + +3. 大量“速度限制”其实不是 API rate limit,而是远程 SSH / 同步 / 批量任务的并发保护。 + - 这些限制用于避免同时打爆子机、SSH、MySQL、Redis 或 Nexus 自身 worker。 + - 用户正常浏览后台 API 不会被这些限制直接 429。 + +## 1. 全局 API 限速 + +### 当前状态 + +没有发现全局限速。 + +检索点: + +- `rate_limit` / `ratelimit` / `throttle` / `slowapi` / `limiter` +- `status_code=429` +- FastAPI middleware +- Docker / Uvicorn worker 配置 + +### 影响 + +- 好处:后台操作不会因为普通访问频率触发全局 429。 +- 风险:如果公网暴露,普通 API 缺少统一抗刷保护;目前主要依赖登录锁定、鉴权、业务级并发控制。 + +## 2. 登录接口限速 / 锁定 + +### 位置 + +- `server/api/auth.py:127-156` +- `server/application/services/auth_service.py:37-39` +- `server/application/services/auth_service.py:99-107` +- `server/infrastructure/database/admin_repo.py:41-53` + +### 规则 + +```text +MAX_LOGIN_FAILURES = 5 +LOCKOUT_MINUTES = 15 +``` + +逻辑: + +```text +/api/auth/login + -> AuthService.login() + -> count_recent_failures(username, ip_address, 15) + -> 最近 15 分钟同 username 失败次数 >= 5 + -> 返回 reason=account_locked + -> API 层转成 HTTP 429 +``` + +仓库实现实际按 username 统计最近失败次数: + +```text +LoginAttempt.username == username +LoginAttempt.success == False +LoginAttempt.attempted_at >= cutoff +``` + +备注:白名单 IP 会跳过失败计数锁定。 + +### 影响 + +这是登录防爆破,不是普通 API 限速。 +如果登录时出现 429,含义是账号登录失败过多锁定 15 分钟。 + +## 3. 脚本回调接口限速 + +### 位置 + +- `server/api/agent.py:244-264` +- `server/infrastructure/redis/script_callback_rate.py:14-16` +- `server/infrastructure/redis/script_callback_rate.py:31-44` + +### 规则 + +```text +每 job_id:10 次 / 60 秒 +每 IP:60 次 / 60 秒 +``` + +Redis key: + +```text +script_cb:job:{job_id} +script_cb:ip:{ip} +``` + +触发后返回: + +```text +HTTP 429 Too many script callback requests +``` + +### 影响 + +这是专门保护子机长任务脚本回调的,避免未登录回调接口被刷或同一 job 重复回调过多。 +不会影响后台普通 API,也不会影响宝塔一键登录。 + +## 4. 脚本远程执行并发限制 + +### 位置 + +- `server/config.py:66-69` +- `server/api/scripts.py:41-50` +- `server/application/services/script_service.py:254-260` +- `server/application/services/script_service.py:759-761` + +### 当前值 + +```text +SCRIPT_EXEC_BATCH_SIZE = 50 +SCRIPT_EXEC_CONCURRENCY = 10 +``` + +含义: + +- 一批最多 50 台服务器。 +- 每批同时最多 10 个 SSH 执行。 +- `/api/scripts/exec-config` 会返回这些值给前端显示。 + +### 影响 + +这是远程命令执行的并发保护,不是 HTTP API 限速。 +如果一次选很多服务器,会按批次和并发执行,所以看起来“执行速度被限制”。 + +用户已说明 `script_service` 是管理员远程执行命令功能,任意 shell 本身不是问题;这里仅记录并发限制。 + +## 5. 同步/推送并发限制 + +### 位置 + +- `server/api/schemas.py:224-225` +- `server/application/services/sync_engine_v2.py:39` +- `server/application/services/sync_engine_v2.py:113-127` + +### 当前值 + +API schema: + +```text +batch_size: 默认 50,范围 1-200 +concurrency: 默认 10,范围 1-50 +``` + +服务层硬上限: + +```text +MAX_CONCURRENT = 10 +concurrency = min(concurrency, MAX_CONCURRENT) +``` + +### 影响 + +虽然 API 入参允许 concurrency 到 50,但服务层最终会压到最多 10。 +所以同步/推送如果前端或 API 传 50,实际仍最多 10 并发。 +这是一个“配置表现不一致”的点。 + +## 6. 同步校验 verify 并发限制 + +### 位置 + +- `server/api/sync_v2.py:1352-1354` + +### 当前值 + +```text +sem = asyncio.Semaphore(5) +``` + +### 影响 + +同步校验会最多 5 台并发 SSH 校验。 +这也不是 API 429,而是校验任务执行速度保护。 + +## 7. 批量服务器操作并发限制 + +### 位置 + +- `server/application/services/server_batch_service.py:35` +- `server/application/services/server_batch_service.py:427-446` + +### 当前值 + +```text +CONCURRENCY = 5 +``` + +适用操作包括: + +- health-check +- detect-path +- detect-domain +- onboard +- install-agent +- upgrade-agent +- uninstall-agent +- bt-panel-bootstrap + +### 影响 + +批量服务器操作最多 5 台并发。 +宝塔批量 bootstrap 也走这里。 +这不是 HTTP 限速,而是远程任务并发限制。 + +## 8. SSH 健康检查并发限制 + +### 位置 + +- `server/application/server_connectivity.py:124-142` + +### 当前值 + +```text +concurrency = 10 +sem = asyncio.Semaphore(max(1, concurrency)) +``` + +### 影响 + +用于批量 SSH health check,默认 10 并发。 + +## 9. WebSocket 连接数限制 + +### 位置 + +- `server/api/websocket.py:42-53` + +### 当前值 + +```text +MAX_CONNECTIONS = 500 +``` + +超过后关闭连接: + +```text +code=4003, reason="Too many connections" +``` + +### 影响 + +这不是 HTTP API 限速,是单 worker 内存里的 WebSocket 连接数上限。 + +## 10. 前端侧并发/节流 + +### 位置 + +- `frontend/src/api/index.ts`:只处理后端 429 文案,没有主动限速。 +- `frontend/src/utils/filePreload.ts:12`:文件预加载并发 4。 +- `frontend/src/composables/push/types.ts:98`:推送预览并发 4。 +- `frontend/src/composables/push/usePushPreview.ts:75-76`:按 4 个一批预览。 + +### 影响 + +前端没有全局 API throttle/debounce。 +只有局部预加载/预览并发保护。 + +## 11. 部署侧限制 + +### 位置 + +- `Dockerfile:43` + +当前容器命令: + +```text +uvicorn server.main:app --host 0.0.0.0 --port 8600 +``` + +没有发现: + +- gunicorn worker 数配置 +- uvicorn --workers 配置 +- nginx limit_req 配置 +- compose 侧 API rate limit + +### 影响 + +当前 Docker 部署默认单 Uvicorn worker。 +这不是“限速”,但会影响并发吞吐;远程 SSH 操作如果阻塞资源,也可能让 API 响应变慢。 + +## 12. 如果要调整,先给 6 个方向 + +后续如果要改,我建议先选方向,不直接动代码: + +### 方案 A:不加全局限速,只调远程任务并发 + +- 调整脚本执行、同步、批量任务并发。 +- 适合内网/自用后台。 +- 风险低,不影响普通 API。 + +### 方案 B:加登录/敏感接口更严格限速 + +- 保持普通 API 不限速。 +- 对登录、令牌刷新、敏感设置、脚本回调加 Redis 限速。 +- 安全收益高,误伤较少。 + +### 方案 C:加全局 Redis 限速,但白名单内网/IP + +- 对所有 API 做每 IP / 每用户限速。 +- 内网或管理员 IP 放宽。 +- 更安全,但可能误伤批量操作和前端轮询。 + +### 方案 D:按接口分层限速 + +- 读接口宽松。 +- 写接口中等。 +- 登录/敏感/远程执行严格。 +- 最合理,但实现和测试工作量最大。 + +### 方案 E:只优化吞吐,不做 rate limit + +- 增加 uvicorn/gunicorn workers。 +- 调 DB pool、Redis、SSH 线程池/并发。 +- 目标是更快,不是防刷。 + +### 方案 F:把后台远程任务全部队列化 + +- API 只创建任务,worker 队列执行。 +- 支持取消、重试、并发池、优先级。 +- 长期最稳,但架构改动最大。 + +## 当前建议 + +如果你的目标是“后台 API 不要被限速”,当前代码已经没有全局 API 限速。 +真正影响速度的主要是: + +1. 同步服务硬上限 `MAX_CONCURRENT = 10`。 +2. 批量服务器操作 `CONCURRENCY = 5`。 +3. 脚本执行 `SCRIPT_EXEC_CONCURRENCY = 10`、`SCRIPT_EXEC_BATCH_SIZE = 50`。 +4. Docker 默认单 Uvicorn worker。 + +如果你的目标是“提高批量操作速度”,优先讨论方案 A 或 E。 +如果你的目标是“增强公网安全”,优先讨论方案 B 或 D。 diff --git a/docs/release/20260708/Nexus-SSH命令执行安全巡检阶段1-20260707.md b/docs/release/20260708/Nexus-SSH命令执行安全巡检阶段1-20260707.md new file mode 100644 index 00000000..c87a6c57 --- /dev/null +++ b/docs/release/20260708/Nexus-SSH命令执行安全巡检阶段1-20260707.md @@ -0,0 +1,175 @@ +# Nexus SSH/文件管理安全巡检阶段 1(2026-07-07) + +## 1. 巡检口径调整 + +用户已明确:`script_service` 是“管理员远程执行命令”功能,天然允许管理员执行任意 shell;这是产品设计内能力,不作为本轮漏洞修复项。 + +后续对它只保留三类审查: + +- 鉴权:必须只能由已登录管理员调用。 +- 审计:执行人、目标服务器、执行时间、结果摘要要可追踪。 +- 敏感信息:不要把密码、Token、密钥等完整写入日志或返回给不该看到的人。 + +本轮实际修复的是“非脚本服务、非预期的命令参数边界”。 + +--- + +## 2. 当前测试机状态 + +- 测试机:`192.168.124.219` +- 项目目录:`/opt/nexus-dev-current` +- Git 分支:`audit/security-review` +- 当前最新提交:`bcec78f fix: harden file manager shell operations` +- Nexus 容器:`0.0.0.0:18600 -> 8600`,健康检查返回 `ok` +- MySQL:`0.0.0.0:13306 -> 3306` +- Redis:`0.0.0.0:16379 -> 6379` + +最近提交: + +```text +bcec78f fix: harden file manager shell operations +112e617 fix: stop archive option injection via filenames +99edd30 chore: default compose ports for nexus test deploy +a538358 fix: keep bt panel sessions alive after one-click login +9d42fcd chore: baseline Nexus LAN test deployment snapshot +``` + +--- + +## 3. 已完成修复 A:远程压缩命令参数终止符 + +提交:`112e617 fix: stop archive option injection via filenames` + +修改文件: + +- `server/infrastructure/ssh/remote_archive.py` +- `tests/test_remote_archive_commands.py` + +修复点: + +- `tar` 压缩命令增加 `--`,避免成员名以 `-` 开头时被当作 tar 参数。 +- `zip` 压缩命令增加 `--`,避免目标名或成员名以 `-` 开头时被当作 zip 参数。 + +运行容器已热更新确认包含: + +```text +tar -czf ... -C ... -- ... +zip -qr -- ... +``` + +--- + +## 4. 已完成修复 B:文件管理命令参数终止符 + +提交:`bcec78f fix: harden file manager shell operations` + +修改文件: + +- `server/api/sync_v2.py` + +加固命令: + +```text +rm -rf -- +mv -- +mkdir -p -- +cp -r -- +mv -t -- +``` + +说明: + +- 这些路径本身已经经过 `normalize_remote_abs_path()` 和 `shlex.quote()`。 +- 本次增加 `--` 是第二道防线,防止未来路径来源变化或边界输入导致工具把路径误判为命令选项。 +- 这不是限制管理员能力;管理员确实要执行任意命令时仍应通过设计内的 `script_service`。 + +--- + +## 5. 已完成修复 C:递归 chmod/chown 系统目录子树保护 + +提交:`bcec78f fix: harden file manager shell operations` + +修改文件: + +- `server/utils/files_chmod_policy.py` +- `tests/test_file_permissions.py` + +修复前: + +- 只禁止对精确系统路径递归改权限,例如 `/etc`、`/usr`、`/`。 +- 但 `/etc/nginx`、`/usr/local` 这类系统目录子树仍可能被文件管理 UI 执行递归 chmod/chown。 + +修复后: + +- 继续禁止精确系统路径。 +- 新增禁止关键系统目录子树: + - `/bin/` + - `/boot/` + - `/dev/` + - `/etc/` + - `/lib/` + - `/lib64/` + - `/proc/` + - `/run/` + - `/sbin/` + - `/sys/` + - `/usr/` + - `/var/cache/` + - `/var/lib/` + - `/var/log/` + - `/var/run/` + - `/var/spool/` +- 保留常见 Web 目录可操作: + - `/www/wwwroot/site` + - `/var/www/html` + +设计原因: + +- 文件管理 UI 的 chmod/chown 是“安全操作封装”,应防止误点造成系统不可用。 +- 真正需要改系统目录权限的管理员,仍可使用设计内的 `script_service` 明确执行命令。 + +--- + +## 6. 验证结果 + +已运行测试: + +```text +pytest tests/test_file_permissions.py tests/test_schema_path_validators.py tests/test_remote_archive_commands.py -q +22 passed in 0.54s +``` + +完整后端测试: + +```text +pytest -q +733 passed, 1 skipped in 11.77s +``` + +格式检查: + +```text +git diff --check +无输出,表示无 whitespace 错误。 +``` + +运行容器热更新: + +- 已把以下文件复制进 `nexus-nexus-1` 容器: + - `server/infrastructure/ssh/remote_archive.py` + - `server/api/sync_v2.py` + - `server/utils/files_chmod_policy.py` +- 已重启 Nexus 容器。 +- 健康检查返回 `ok`。 + +--- + +## 7. 下一步继续巡检建议 + +下一阶段建议按以下顺序继续: + +1. `server/api/sync_v2.py` 解压链路:检查 tar/zip 解压是否需要增加归档成员路径校验,防 zip-slip/tar traversal。 +2. `server/application/services/server_file_transfer_service.py`:检查跨服务器打包/投递的路径校验、临时目录清理、权限回退。 +3. `server/infrastructure/ssh/remote_shell.py`、`asyncssh_pool.py`:复核 timeout、sudo fallback、stderr/stdout 截断和敏感信息处理。 +4. `server/infrastructure/btpanel/ssh_bootstrap.py`、`ssh_login.py`:复核宝塔 token、白名单、session patch 输出中是否有敏感信息泄漏。 +5. `server/api/servers.py`:复核 agent 安装/升级命令拼接、参数来源、失败回滚边界。 diff --git a/docs/release/20260708/Nexus-SSH命令执行安全巡检阶段2-安全解压-20260707.md b/docs/release/20260708/Nexus-SSH命令执行安全巡检阶段2-安全解压-20260707.md new file mode 100644 index 00000000..56bcf4ba --- /dev/null +++ b/docs/release/20260708/Nexus-SSH命令执行安全巡检阶段2-安全解压-20260707.md @@ -0,0 +1,177 @@ +# Nexus SSH/文件管理安全巡检阶段 2:安全解压(2026-07-07) + +## 1. 本阶段目标 + +继续 SSH/文件管理安全巡检,重点检查: + +- `/api/sync/decompress` 解压链路。 +- tar/zip 归档成员是否可能写出目标目录。 +- 归档中的软链接、硬链接、设备文件是否可能造成覆盖或越权写入。 + +`script_service` 仍按用户确认的口径处理:它是设计内管理员远程 shell,不作为漏洞;本阶段只加固文件管理 UI 的“封装式安全操作”。 + +--- + +## 2. 修复提交 + +提交: + +```text +845d7e2 fix: validate remote archives before extraction +``` + +修改文件: + +```text +server/api/sync_v2.py +server/infrastructure/ssh/remote_archive.py +tests/test_remote_archive_commands.py +``` + +--- + +## 3. 修复前风险 + +原解压逻辑直接执行: + +```bash +tar xzf -C +unzip -o -d +``` + +虽然 `` 和 `` 已经做过绝对路径规范化和 shell quote,但归档文件内部成员名不受 Nexus 控制,仍需要额外检查: + +- 成员名为 `../evil` 可能尝试路径穿越。 +- 成员名为 `/etc/passwd` 属于绝对路径。 +- zip 中 Windows 分隔符 `dir\evil` 在某些工具/平台语义下容易产生歧义。 +- 归档内软链接/硬链接/设备文件可能造成解压后写出目标目录或制造特殊文件。 + +--- + +## 4. 修复后设计 + +解压流程从 API 中下沉到: + +```text +server/infrastructure/ssh/remote_archive.py::run_remote_decompress() +``` + +新流程: + +1. 判断格式: + - `.tar.gz` / `.tgz` -> `tar.gz` + - `.zip` -> `zip` +2. 先列出成员名: + - tar:`tar -tzf ` + - zip:`unzip -Z1 ` +3. 校验成员名: + - 禁止空文件名。 + - 禁止绝对路径。 + - 禁止 `..` 路径穿越。 + - 禁止 Windows 反斜杠分隔符。 +4. 再列出成员类型: + - tar:`tar -tvzf ` + - zip:`zipinfo -l ` +5. 校验成员类型: + - 允许普通文件和目录。 + - 禁止符号链接。 + - 禁止硬链接。 + - 禁止块设备、字符设备、FIFO、socket。 +6. 通过校验后才解压。 + +安全 tar 解压命令增加: + +```bash +tar --no-same-owner --no-same-permissions -xzf -C +``` + +避免归档强行恢复 owner/permission,降低解压后权限异常风险。 + +--- + +## 5. 新增测试覆盖 + +新增/扩展测试文件: + +```text +tests/test_remote_archive_commands.py +``` + +覆盖: + +- tar/zip 成员列表命令。 +- tar 解压命令使用 `--no-same-owner --no-same-permissions`。 +- 拒绝 `../evil.txt`。 +- 拒绝 `/etc/passwd`。 +- 拒绝 `dir\evil.txt`。 +- 允许普通相对文件和目录。 +- 拒绝符号链接。 +- 拒绝字符设备。 +- 允许 zipinfo header/footer + 普通文件/目录。 + +--- + +## 6. 验证结果 + +相关测试: + +```text +pytest tests/test_remote_archive_commands.py tests/test_schema_path_validators.py tests/test_file_permissions.py -q +31 passed in 0.55s +``` + +完整后端测试: + +```text +pytest -q +742 passed, 1 skipped in 11.83s +``` + +格式检查: + +```text +git diff --check +无输出 +``` + +运行容器热更新: + +- 已复制以下文件到 `nexus-nexus-1`: + - `server/api/sync_v2.py` + - `server/infrastructure/ssh/remote_archive.py` +- 已重启 Nexus 容器。 +- `http://127.0.0.1:18600/health` 返回 `ok`。 +- 容器内已确认存在: + - `validate_archive_member_names` + - `run_remote_decompress` + - API 调用 `run_remote_decompress(...)` + +--- + +## 7. 当前最新提交链 + +```text +845d7e2 fix: validate remote archives before extraction +bcec78f fix: harden file manager shell operations +112e617 fix: stop archive option injection via filenames +99edd30 chore: default compose ports for nexus test deploy +a538358 fix: keep bt panel sessions alive after one-click login +``` + +--- + +## 8. 下一阶段建议 + +继续巡检: + +1. `server/application/services/server_file_transfer_service.py` + - 跨服务器打包/拉取/投递路径校验。 + - 临时文件清理。 + - sudo fallback 边界。 +2. `server/infrastructure/ssh/remote_shell.py` / `asyncssh_pool.py` + - timeout 默认值。 + - stdout/stderr 截断。 + - 敏感信息是否可能返回前端。 +3. `server/api/servers.py` + - agent 安装/升级/回滚命令拼接。 + - 参数来源和 quote。 diff --git a/docs/release/20260708/Nexus-SSH命令执行安全巡检阶段3-文件传输与专用Skill-20260707.md b/docs/release/20260708/Nexus-SSH命令执行安全巡检阶段3-文件传输与专用Skill-20260707.md new file mode 100644 index 00000000..682f94cf --- /dev/null +++ b/docs/release/20260708/Nexus-SSH命令执行安全巡检阶段3-文件传输与专用Skill-20260707.md @@ -0,0 +1,87 @@ +# Nexus SSH 命令执行安全巡检阶段 3:跨服务器文件传输与项目专用 Skill + +日期:2026-07-07 +分支:`audit/security-review` +范围:跨服务器文件传输、BT 面板直链下载投递、远程归档压缩/解压、项目专用审查 Skill。 + +## 1. 本阶段结论 + +本阶段补齐了跨服务器文件传输链路中遗留的高风险边界: + +- 传输包在目标服务器解压时,复用了统一的安全解压流程,先列成员、校验路径和类型,再解压。 +- 远程 `mkdir`、`mv`、`rm`、归档 staging move 增加 `--` 参数终止符,避免以 `-` 开头的路径被当成命令选项。 +- `pull_transfer_package` 与 BT 直链投递目标路径改为统一 `normalize_remote_abs_path` 校验,拒绝 `..` 与反斜杠。 +- 新增 `.skills/` 下 Nexus 专用安全巡检规范,后续每轮审查可按固定规则执行。 + +完整测试结果:`748 passed, 1 skipped`。 + +## 2. 调用链 + +### 2.1 Nexus 代理下载投递 + +```mermaid +flowchart TD + A["API: transfer package/deliver"] --> B["server_file_transfer_service.create_transfer_package"] + B --> C["run_remote_compress(source)"] + C --> D["Redis transfer_package_store: token/meta/ttl"] + D --> E["pull_transfer_package(dest)"] + E --> F["curl Nexus signed transfer URL to /tmp staging"] + F --> G["mv -- staging -> dest archive path"] + G --> H["run_remote_decompress(dest): list members"] + H --> I["validate names/types"] + I --> J["safe tar/unzip extract"] + J --> K["apply_transfer_permissions"] +``` + +### 2.2 BT 面板直链下载投递 + +```mermaid +flowchart TD + A["deliver_transfer_package"] --> B["source has BT API credentials"] + B --> C["_deliver_via_btpanel_download"] + C --> D["run_remote_compress(source)"] + D --> E["signed_download_url(BT)"] + E --> F["dest curl BT download URL to /tmp staging"] + F --> G["mv -- staging -> dest archive path"] + G --> H["run_remote_decompress(dest)"] + H --> I["validate archive member names/types"] + I --> J["extract + permissions"] +``` + +## 3. 修复点 + +| 文件 | 修复 | +|---|---| +| `server/application/services/server_file_transfer_service.py` | `_extract_archive_on_server()` 改为调用 `run_remote_decompress()`,解压前校验归档成员;目标路径使用 `normalize_remote_abs_path()`;`mkdir/mv/rm` 增加 `--`。 | +| `server/infrastructure/ssh/remote_archive.py` | tar/zip staging 压缩后的 `mv` 改为 `mv -f -- ...`。 | +| `tests/test_server_file_transfer.py` | 新增安全解压委托、解压校验错误、目标路径穿越拒绝、下载 staging move/rm 参数终止符测试;更新旧的 tar overwrite 行为测试。 | +| `tests/test_remote_archive_commands.py` | 新增 tar/zip staging move 参数终止符测试。 | +| `.skills/nexus-security-review/SKILL.md` | 固化 Nexus 安全巡检总规则。 | +| `.skills/nexus-ssh-safety/SKILL.md` | 固化 SSH 命令、路径、归档安全规则。 | +| `.skills/nexus-btpanel-review/SKILL.md` | 固化宝塔一键登录/凭据/会话修复审查规则。 | + +## 4. 边界说明 + +- `script_service` 是管理员显式远程 shell 能力,本阶段没有把它作为命令注入漏洞处理;后续只审查鉴权、审计、超时、输出截断和脱敏。 +- 文件传输仍允许管理员选择任意绝对目标路径,但不允许路径字符串中包含 `..`、反斜杠或非绝对路径。 +- 归档解压会拒绝绝对路径、父目录穿越、Windows 反斜杠、符号链接、硬链接和设备/FIFO/socket 等特殊条目。 +- 未在报告中记录任何密码、token、cookie、SSH key 或 BT API key。 + +## 5. 验证记录 + +```bash +.venv/bin/python -m pytest tests/test_server_file_transfer.py tests/test_remote_archive_commands.py -q +# 40 passed + +.venv/bin/python -m pytest -q +# 748 passed, 1 skipped + +git diff --check +# pass +``` + +## 6. 下一步建议 + +1. 继续审查 `server/api/servers.py` 的 agent 安装/升级/回滚命令拼接与超时边界。 +2. 审查 `remote_shell.py` / `asyncssh_pool.py` 的 stdout/stderr 截断、敏感输出脱敏和 sudo fallback 一致性。 +3. 审查 API 鉴权覆盖:确认所有管理接口均有管理员依赖,公开下载类接口只暴露必要的 token 校验。 diff --git a/docs/release/20260708/Nexus-app-v2-双后台实施说明-20260707.md b/docs/release/20260708/Nexus-app-v2-双后台实施说明-20260707.md new file mode 100644 index 00000000..23ca145c --- /dev/null +++ b/docs/release/20260708/Nexus-app-v2-双后台实施说明-20260707.md @@ -0,0 +1,1903 @@ +# Nexus /app-v2 双后台实施说明(2026-07-07) + +## 结论 + +已在 Nexus 源码快照中新增第二套后台 /app-v2,并保留旧后台 /app/ 不动。当前 /app-v2 已从纯静态 mock 版推进到“真实 API 优先 + 演示数据兜底”的首屏版本: + +- 旧后台:/app/,继续承载完整生产功能。 +- 新后台:/app-v2/,React + TypeScript + Vite + Tailwind + shadcn 风格组件。 +- 服务器资产、宝塔状态、一键登录已接入现有后端 API。 +- 一键登录仍走后端既有逻辑,因此会触发登录前宝塔 session TTL 兜底检测 / 修复。 +- 非 401 API 异常会 fallback 到演示数据;401 或未登录会显示登录保护卡片。 + +## 当前代码位置 + +项目目录: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 + +新后台源码目录: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\frontend-v2 + +构建输出目录: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\web\app-v2 + +预览截图: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\ui-mockups\nexus-app-v2-implemented-20260707.png + +## 技术栈 + +| 模块 | 选择 | +|---|---| +| 前端框架 | React | +| 类型系统 | TypeScript strict | +| 构建工具 | Vite | +| 样式 | Tailwind CSS + shadcn 风格原子组件 | +| 请求缓存 | TanStack Query | +| 表格 | TanStack Table | +| 状态管理 | Zustand | +| 图标 | lucide-react | +| 命令面板 | cmdk | + +## 后端静态挂载 + +文件: + + server/main.py + +已挂载: + + WEB_APP_V2_DIR = ROOT_DIR / "web" / "app-v2" + if WEB_APP_V2_DIR.is_dir(): + app.mount("/app-v2", StaticFiles(directory=str(WEB_APP_V2_DIR), html=True), name="static_app_v2") + +## Docker 构建 + +文件: + + Dockerfile + Dockerfile.prod + +处理方式: + +- Dockerfile 增加 web/app-v2 静态目录复制。 +- Dockerfile.prod 增加 frontend-v2 build stage,并把构建产物复制到镜像内 web/app-v2。 + +## 本轮新增 / 修改的关键文件 + +前端 V2: + + frontend-v2/src/features/dashboard/api/dashboardApi.ts + frontend-v2/src/features/dashboard/hooks/useDashboardData.ts + frontend-v2/src/features/dashboard/DashboardPage.tsx + frontend-v2/src/features/dashboard/components/AppShell.tsx + frontend-v2/src/features/dashboard/components/CommandPalette.tsx + frontend-v2/src/features/dashboard/components/ServerTable.tsx + frontend-v2/src/features/dashboard/components/ServerDrawer.tsx + frontend-v2/src/features/dashboard/components/TtlMatrix.tsx + frontend-v2/src/features/dashboard/components/TaskCenter.tsx + frontend-v2/src/features/dashboard/components/TrendPanel.tsx + frontend-v2/src/features/dashboard/components/HealthScore.tsx + frontend-v2/src/features/dashboard/components/AuditTimeline.tsx + frontend-v2/src/features/dashboard/data/dashboardData.ts + +核心工程: + + frontend-v2/src/lib/api.ts + frontend-v2/src/stores/appStore.ts + server/main.py + Dockerfile + Dockerfile.prod + +## 已接入的真实 API + +### 1. 服务器列表 + +请求: + + GET /api/servers/?page=1&per_page=100&sort_by=id&sort_order=desc + +用途: + +- 生成服务器资产表格。 +- 生成服务器详情抽屉。 +- 计算在线服务器、风险告警、心跳延迟等指标。 + +### 2. 宝塔状态列表 + +请求: + + GET /api/btpanel/servers + +用途: + +- 合并宝塔 configured / verify_ssl / bootstrap_state。 +- 合并 session_cleanup_ttl_ok。 +- 合并 session_cleanup_ttl_patched。 +- 合并 session_cleanup_ttl_checked_at 和 last_error。 +- 驱动 TTL 健康矩阵、任务中心和详情诊断。 + +### 3. 宝塔一键登录 + +请求: + + POST /api/btpanel/servers/{server_id}/login-url + +前端行为: + +- 表格操作列可触发。 +- 详情抽屉快捷操作可触发。 +- Ctrl/Cmd + K 命令面板可触发。 +- 成功后使用新标签页打开返回的登录 URL。 +- 页面提示“后端已执行 TTL 兜底检测”。 + +后端关键点: + +- V2 没有绕过后端,也没有在前端生成宝塔 token。 +- 仍调用既有 create_login_url 逻辑。 +- 该后端逻辑会在一键登录前调用 session cleanup TTL 检测 / 修复。 +- 因此新增宝塔也能在一键登录前走 hardcoded_3600 兜底检测 / 修复路径。 + +## 登录与安全策略 + +V2 当前策略: + +- 不在前端展示全局 API Key。 +- 不把 API Key、JWT、Cookie、宝塔 tmp_login token、私钥等敏感值写入页面展示。 +- access token 只放内存。 +- 页面启动时调用 /api/auth/refresh 通过 HttpOnly refresh cookie 恢复会话。 +- 成功后再请求 /api/auth/me。 +- 未登录或 401 时显示登录保护,不展示后台数据。 +- 非 401 API 失败时 fallback 到演示数据,方便本地预览和灰度 UI 验证。 + +## 当前 V2 首屏能力 + +| 区块 | 状态 | 说明 | +|---|---:|---| +| 左侧导航 | 已完成 | 未迁移功能先回跳 /app/ | +| 顶部搜索入口 | 已完成 | Ctrl/Cmd + K 命令面板 | +| 资产统计卡片 | 已接真实数据 | 在线服务器、宝塔修复、活跃任务、风险告警 | +| 健康评分 | 已接真实数据 | 根据在线率、TTL 修复率、风险数计算 | +| 资产健康趋势 | 已完成 | 轻量 SVG 趋势图 | +| 宝塔 TTL 健康矩阵 | 已接真实数据 | 根据 session_cleanup_ttl_ok / patched / online 着色 | +| 任务中心 | 已接真实数据 | TTL、SSL、Agent 进度按当前资产计算 | +| 服务器表格 | 已接真实数据 | TanStack Table,可选中服务器,可一键登录 | +| 服务器详情抽屉 | 已接真实数据 | CPU、内存、磁盘、心跳、TTL、SSL、错误信息 | +| 审计时间线 | 演示数据 | 后续可接审计日志 API | +| 迁移看板 | 已完成 | 明确双后台并行进度 | + +## 构建验证 + +执行目录: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\frontend-v2 + +已执行并通过: + + npm.cmd run build + +构建输出包括: + + web/app-v2/index.html + web/app-v2/assets/index-*.css + web/app-v2/assets/react-*.js + web/app-v2/assets/query-*.js + web/app-v2/assets/table-*.js + web/app-v2/assets/ui-*.js + web/app-v2/assets/DashboardPage-*.js + +后端语法验证也已通过: + + python -m py_compile server/main.py + +## 发布状态 + +当前改动已在本地源码快照中完成并构建通过,但没有发布到线上域名,也没有部署到 https://api.synaglobal.vip。 + +如果要上线,需要单独执行发布步骤: + +1. 确认当前源码快照是要发布的版本。 +2. 构建 Docker 镜像或同步 web/app-v2 静态目录。 +3. 部署到目标 Nexus 后端。 +4. 访问 /app-v2/ 做登录、真实 API、宝塔一键登录抽检。 +5. 保留 /app/ 回退入口,确认旧后台不受影响。 + +## 下一步建议 + +1. 在本地或测试机启动 Nexus 后端,访问 /app-v2/ 做真实登录态验证。 +2. 抽检 42.193.182.190、120.79.213.154、192.168.124.219 的一键登录链路。 +3. 对 /api/btpanel/servers 返回字段做一次真实数据快照校验,确认字段名与前端映射完全一致。 +4. 继续迁移宝塔面板页、服务器资产详情页、任务中心详情页。 +5. 给 /app-v2 增加专用 favicon、错误页、空状态和更丰富的 shadcn 风格交互组件。 + +## 2026-07-07 继续推进:服务器资产中心 + +本轮继续把 /app-v2 从“单页看板”推进为“可逐页替换的第二后台”: + +### 已新增 + +- 新增 V2 页面:服务器资产中心。 +- 侧栏支持 V2 内部模块切换: + - 总览 Dashboard:仍留在 /app-v2; + - 服务器资产:进入新资产中心; + - 宝塔面板、任务中心、终端/文件、安全巡检、审计日志、系统设置:暂时跳回 /app/,保证功能不减少。 +- 资产中心新增: + - 名称 / IP / 区域 / TTL 搜索; + - 宝塔状态筛选; + - 风险等级筛选; + - 在线资产、TTL 兜底通过、已修复宝塔、需关注等指标卡; + - 一键登录仍调用后端 /api/btpanel/servers/{id}/login-url; + - 页面内明确提示:前端不生成宝塔 token,后端登录前继续执行 hardcoded_3600 TTL 兜底检测/修复。 + +### 变更文件 + +- `frontend-v2/src/stores/appStore.ts` +- `frontend-v2/src/features/dashboard/components/AppShell.tsx` +- `frontend-v2/src/features/dashboard/components/AssetInventoryPage.tsx` +- `frontend-v2/src/features/dashboard/DashboardPage.tsx` + +### 验证 + +- `npm.cmd run build` 通过。 +- `python -m py_compile server/main.py` 通过。 +- 构建产物已更新到 `web/app-v2/`。 + +### 当前边界 + +- 仍未发布到线上 `https://api.synaglobal.vip`。 +- 资产中心当前复用已有服务器/宝塔状态 API,批量编辑、凭据管理、深度审计等高风险功能仍回退旧后台,后续逐页迁移。 + +## 2026-07-07 继续推进:宝塔面板 V2 + +本轮继续迁移第三个 V2 页面:宝塔面板 / 登录稳定性中心。 + +### 已新增 + +- 新增 V2 页面:宝塔登录稳定性中心。 +- 侧栏“宝塔面板”从旧后台回退改为 /app-v2 内部页面。 +- 页面展示: + - 已配置宝塔数量; + - TTL 兜底通过数量; + - 待兜底修复数量; + - 面板 SSL 建议数量; + - 每台宝塔的面板地址、Session TTL、最近检测、Bootstrap 状态、最近错误。 +- 页面内的一键登录仍复用现有后端接口: + - `POST /api/btpanel/servers/{id}/login-url` +- V2 前端仍不保存、不显示: + - 宝塔密码; + - Cookie; + - tmp_login token; + - 全局 API Key。 +- 宝塔配置编辑、批量导入、危险配置修改仍回旧后台,保证迁移期间功能不减少。 + +### 变更文件 + +- `frontend-v2/src/features/dashboard/components/BtPanelPage.tsx` +- `frontend-v2/src/features/dashboard/components/AppShell.tsx` +- `frontend-v2/src/features/dashboard/DashboardPage.tsx` +- `frontend-v2/src/stores/appStore.ts` +- `frontend-v2/src/features/dashboard/api/dashboardApi.ts` +- `frontend-v2/src/features/dashboard/data/dashboardData.ts` + +### 验证 + +- `npm.cmd run build` 通过。 +- `python -m py_compile server/main.py` 通过。 +- 构建产物已更新到 `web/app-v2/`。 + +### 当前 /app-v2 已迁移页面 + +1. 总览 Dashboard。 +2. 服务器资产中心。 +3. 宝塔登录稳定性中心。 + +### 仍回退旧后台的模块 + +- 任务中心深度详情; +- 终端 / 文件; +- 安全巡检详情; +- 审计日志完整查询; +- 系统设置; +- 宝塔凭据编辑和批量导入。 + + +## 2026-07-07 继续推进:任务中心 V2 + +本轮继续迁移第四个 V2 页面:任务中心 / 自动化运行态。 + +### 已新增 + +- 新增 V2 页面:任务中心 / 自动化运行态。 +- 侧栏“任务中心”从旧后台回退改为 /app-v2 内部页面。 +- 页面展示以下任务态: + - BT TTL 自动巡检:展示 hardcoded_3600 兜底状态; + - 宝塔一键登录兜底队列:强调一键登录前仍由后端执行 TTL 检测/修复; + - 面板 SSL 建议扫描:识别公网宝塔 SSL 建议项; + - Agent 心跳同步:展示在线、CPU、内存、磁盘等资产同步基础; + - 服务器资产风险同步:汇总离线、TTL、SSL、宝塔配置风险; + - 脚本执行审计同步:仅展示入口和迁移状态。 +- 新增任务筛选:全部任务、运行中、已完成、需要关注、排队中。 +- 新增任务搜索:可按任务名、类型、状态、描述搜索。 + +### 安全边界 + +- V2 任务中心第一版只做只读运行态展示,不直接调用脚本执行 API。 +- 脚本执行、停止、重试、危险写操作继续回旧后台 /app/。 +- 前端仍不展示、不保存:密码、Cookie、Token、tmp_login token、全局 API Key、私钥等敏感信息。 +- 宝塔一键登录没有迁移到任务页直接执行;仍在资产页/宝塔页复用后端 POST /api/btpanel/servers/{id}/login-url,保留后端 Redis lock、TTL 兜底检测/修复和审计链路。 + +### 变更文件 + +- frontend-v2/src/features/dashboard/components/TaskCenterPage.tsx +- frontend-v2/src/features/dashboard/components/AppShell.tsx +- frontend-v2/src/features/dashboard/DashboardPage.tsx +- frontend-v2/src/stores/appStore.ts + +### 验证 + +- npm.cmd run build 通过。 +- 构建产物已更新到 web/app-v2/。 + +### 当前 /app-v2 已迁移页面 + +1. 总览 Dashboard。 +2. 服务器资产中心。 +3. 宝塔登录稳定性中心。 +4. 任务中心 / 自动化运行态。 + +### 仍回退旧后台的模块 + +- 任务中心的脚本执行、停止、重试、执行详情深查; +- 终端 / 文件; +- 安全巡检详情; +- 审计日志完整查询; +- 系统设置; +- 宝塔凭据编辑和批量导入。 + + +## 2026-07-07 继续推进:审计日志 V2 + +本轮继续迁移第五个 V2 页面:审计日志 / 告警时间线。 + +### 已新增 + +- 新增 V2 页面:审计日志 / 告警时间线。 +- 侧栏“审计日志”从旧后台回退改为 /app-v2 内部页面。 +- 页面读取现有后端接口: + - GET /api/audit/ + - GET /api/alert-history/ +- 页面将管理员审计记录和告警/恢复记录合并为只读时间线。 +- 新增审计筛选:全部、管理员、服务器、宝塔、安全、告警。 +- 新增搜索:可搜索动作、管理员、目标、IP、详情、时间。 +- 新增指标卡:审计记录、告警记录、需关注、宝塔相关。 + +### 安全边界 + +- V2 审计日志只做只读查询,不新增删除、导出、批量清理等写操作。 +- 页面会对疑似敏感字段做前端二次隐藏,包括 password、token、cookie、secret、api key、private key 等模式。 +- 前端仍不展示、不保存:密码、Cookie、Token、tmp_login token、全局 API Key、私钥等敏感信息。 +- 如果审计/告警 API 异常,页面只切换演示 fallback,不影响旧后台完整查询。 + +### 变更文件 + +- frontend-v2/src/features/dashboard/components/AuditLogPage.tsx +- frontend-v2/src/features/dashboard/api/dashboardApi.ts +- frontend-v2/src/features/dashboard/components/AppShell.tsx +- frontend-v2/src/features/dashboard/DashboardPage.tsx +- frontend-v2/src/stores/appStore.ts + +### 验证 + +- npm.cmd run build 通过。 +- 构建产物已更新到 web/app-v2/。 + +### 当前 /app-v2 已迁移页面 + +1. 总览 Dashboard。 +2. 服务器资产中心。 +3. 宝塔登录稳定性中心。 +4. 任务中心 / 自动化运行态。 +5. 审计日志 / 告警时间线。 + +### 仍回退旧后台的模块 + +- 任务中心的脚本执行、停止、重试、执行详情深查; +- 终端 / 文件; +- 安全巡检详情; +- 系统设置; +- 宝塔凭据编辑和批量导入。 + + +## 2026-07-07 继续推进:安全巡检 V2 + +本轮继续迁移第六个 V2 页面:安全巡检 / 风险总览。 + +### 已新增 + +- 新增 V2 页面:安全巡检 / 风险总览。 +- 侧栏“安全巡检”从旧后台回退改为 /app-v2 内部页面。 +- 页面复用当前资产、宝塔状态、审计和告警信号,形成只读巡检项: + - 宝塔 Session TTL 兜底; + - 公网宝塔面板 SSL; + - Agent 在线与资产同步; + - 审计/告警异常事件; + - 管理员远程执行命令边界; + - 敏感信息前端可见性。 +- 新增巡检筛选:全部、严重、关注、通过、宝塔、SSL、Agent、审计。 +- 新增搜索:可按巡检项、证据、建议、分类和等级搜索。 +- 新增指标卡:巡检得分、严重项、需关注、通过项。 + +### 安全边界 + +- V2 安全巡检当前只做只读风险总览,不触发修复、不修改服务器、不执行脚本。 +- script_service 是管理员远程执行命令能力,允许 shell 属于预期设计;V2 只标注权限/审计边界,不把它作为漏洞处理。 +- 修复 SSL、修改宝塔凭据、执行 shell、清理审计日志等写操作仍回旧后台 /app/。 +- 前端仍不展示、不保存:密码、Cookie、Token、tmp_login token、全局 API Key、私钥等敏感信息。 +- 宝塔一键登录仍在资产页/宝塔页复用后端接口,后端继续负责 TTL 兜底和审计链路。 + +### 变更文件 + +- frontend-v2/src/features/dashboard/components/SecurityInspectionPage.tsx +- frontend-v2/src/features/dashboard/components/AppShell.tsx +- frontend-v2/src/features/dashboard/DashboardPage.tsx +- frontend-v2/src/stores/appStore.ts + +### 验证 + +- npm.cmd run build 通过。 +- 构建产物已更新到 web/app-v2/。 + +### 当前 /app-v2 已迁移页面 + +1. 总览 Dashboard。 +2. 服务器资产中心。 +3. 宝塔登录稳定性中心。 +4. 任务中心 / 自动化运行态。 +5. 审计日志 / 告警时间线。 +6. 安全巡检 / 风险总览。 + +### 仍回退旧后台的模块 + +- 任务中心的脚本执行、停止、重试、执行详情深查; +- 终端 / 文件; +- 系统设置; +- 宝塔凭据编辑和批量导入; +- 会修改服务器状态的深度修复动作。 + + +## 2026-07-07 继续推进:系统设置 V2 + +本轮继续迁移第七个 V2 页面:系统设置只读概览。 + +### 已新增 + +- 新增 V2 页面:系统设置 / 只读概览。 +- 侧栏“系统设置”从旧后台回退改为 /app-v2 内部页面。 +- 新增设置概览 API 聚合: + - GET /api/settings/:读取设置表,后端敏感值继续返回脱敏状态; + - GET /api/settings/ip-allowlist:读取登录白名单状态; + - 任一非认证错误会降级为安全演示数据,不影响旧后台。 +- 页面展示管理员会话、TOTP 状态、全局 API Key 配置状态、登录白名单、宝塔 TTL 兜底、中心 API 地址、心跳阈值。 +- 新增“可安全展示的设置”列表,只列出白名单内的非敏感设置键。 +- 迁移看板更新:系统设置进入 V2 只读迁移阶段,终端 / 文件仍回旧后台。 + +### 安全边界 + +- V2 系统设置当前只做只读概览,不直接执行任何生产配置写入。 +- 全局 API Key 只显示“已配置(隐藏)/未配置”,不显示真实值、不复制、不导出。 +- 前端继续不展示、不保存:密码、Cookie、Token、tmp_login token、全局 API Key、私钥等敏感信息。 +- 凭据编辑、API Key 修改、SSH 私钥、白名单增删、系统级配置写入仍回旧后台 /app/。 +- 宝塔一键登录仍走后端 login-url 接口,登录前 TTL 兜底检测/修复链路未改变。 + +### 变更文件 + +- frontend-v2/src/features/dashboard/components/SettingsOverviewPage.tsx +- frontend-v2/src/features/dashboard/api/dashboardApi.ts +- frontend-v2/src/features/dashboard/components/AppShell.tsx +- frontend-v2/src/features/dashboard/DashboardPage.tsx +- frontend-v2/src/stores/appStore.ts + +### 验证 + +- npm.cmd run build 通过。 +- 构建产物已更新到 web/app-v2/。 +- 敏感关键词扫描仅命中说明性文案、脱敏逻辑和内存 access token 管理代码,未发现写死真实敏感值。 + +### 当前 /app-v2 已迁移页面 + +1. 总览 Dashboard。 +2. 服务器资产中心。 +3. 宝塔登录稳定性中心。 +4. 任务中心 / 自动化运行态。 +5. 审计日志 / 告警时间线。 +6. 安全巡检 / 风险总览。 +7. 系统设置 / 只读概览。 + +### 仍回退旧后台的模块 + +- 终端 / 文件; +- 任务中心的脚本执行、停止、重试、执行详情深查; +- 宝塔凭据编辑和批量导入; +- API Key 修改、SSH 私钥、白名单增删; +- 会修改服务器状态或系统配置的深度修复动作。 + + +## 2026-07-07 继续推进:终端 / 文件 V2 + +本轮继续迁移第八个 V2 页面:终端 / 文件安全入口。 + +### 已新增 + +- 新增 V2 页面:终端 / 文件安全入口。 +- 侧栏“终端 / 文件”从旧后台回退改为 /app-v2 内部页面。 +- 页面按风险分层展示: + - WebSSH 终端; + - 远程文件浏览; + - 文件推送 / 上传; + - 脚本库 / 命令执行; + - 命令日志 / 执行记录; + - 宝塔联动保护。 +- 页面接入 /api/audit/ 和 /api/alert-history/ 的现有数据聚合,筛选终端、文件、脚本相关审计摘要。 +- 审计 detail 在前端二次脱敏后展示,避免把历史命令或文件操作中的密码、Token、Cookie、API Key、私钥片段直接显示出来。 +- 新增能力搜索,可按终端、文件、脚本、审计、安全边界搜索。 +- 保留旧后台入口:/app/terminal、/app/files、/app/push、/app/scripts、/app/commands。 + +### 安全边界 + +- V2 当前不创建 WebSSH 会话,不发送键盘输入。 +- V2 当前不上传、不删除、不写文件、不 chmod、不压缩/解压远端文件。 +- V2 当前不调用 /api/scripts/exec,不绕过后端危险命令检查。 +- script_service 是管理员远程执行 shell 的预期功能,不作为漏洞处理;V2 只强调执行必须继续经过后端和审计链路。 +- 高风险动作包括真实终端、文件写入、上传、删除、停止、重试、脚本执行详情深查,继续回旧后台 /app/。 +- 宝塔一键登录和 TTL hardcoded_3600 兜底修复链路未改,仍在资产/宝塔页调用后端 login-url。 + +### 变更文件 + +- frontend-v2/src/features/dashboard/components/TerminalFilesPage.tsx +- frontend-v2/src/features/dashboard/components/AppShell.tsx +- frontend-v2/src/features/dashboard/DashboardPage.tsx +- frontend-v2/src/stores/appStore.ts + +### 验证 + +- npm.cmd run build 通过。 +- 构建产物已更新到 web/app-v2/。 + +### 当前 /app-v2 已迁移页面 + +1. 总览 Dashboard。 +2. 服务器资产中心。 +3. 宝塔登录稳定性中心。 +4. 任务中心 / 自动化运行态。 +5. 审计日志 / 告警时间线。 +6. 安全巡检 / 风险总览。 +7. 系统设置 / 只读概览。 +8. 终端 / 文件安全入口。 + +### 仍回退旧后台的模块 + +- 真实 WebSSH 交互终端; +- 文件上传、删除、写入、chmod、压缩/解压、跨服务器传输; +- 脚本执行、停止、重试、执行详情深查; +- 宝塔凭据编辑和批量导入; +- API Key 修改、SSH 私钥、白名单增删; +- 会修改服务器状态或系统配置的深度修复动作。 + + +## 2026-07-07 继续推进:执行记录只读中心 V2 + +本轮继续迁移第九个 V2 页面:执行记录只读中心。 + +### 已新增 + +- 新增 V2 页面:执行记录只读中心。 +- 侧栏新增“执行记录”,进入 /app-v2 内部页面,不影响旧后台 /app/。 +- 新增执行记录 API 聚合: + - GET /api/execution-records?limit=80&offset=0; + - 读取失败但非 401 时,降级为只读演示数据; + - 401 继续交给登录态保护处理。 +- 页面使用 TanStack Query 缓存执行记录,使用 TanStack Table 渲染列表。 +- 支持按任务名、操作者、操作、ID 搜索。 +- 支持按记录类型筛选:脚本执行、服务器批量任务。 +- 支持按状态筛选:running、completed、failed、partial、cancelled 以及后端返回的其它状态。 +- 指标卡展示执行记录总量、运行中数量、异常/部分完成数量、涉及服务器数量。 + +### 安全边界 + +- V2 当前只读,不调用 /api/scripts/exec。 +- V2 当前不停止任务、不重试任务、不拉取完整日志、不展示完整命令详情。 +- 列表中的 label、op、command 做前端二次脱敏,隐藏 password、token、cookie、secret、api key、private key、BT_PANEL、BT_KEY、Authorization、Bearer 等片段。 +- 真实脚本执行、停止、重试、完整日志和深度详情继续跳回旧后台 /app/scripts。 +- script_service 仍按用户确认作为“管理员远程执行 shell”的预期能力保留;V2 只控制早期迁移阶段不误触发写操作。 + +### 变更文件 + +- frontend-v2/src/features/dashboard/components/ExecutionRecordsPage.tsx +- frontend-v2/src/features/dashboard/api/dashboardApi.ts +- frontend-v2/src/features/dashboard/components/AppShell.tsx +- frontend-v2/src/features/dashboard/DashboardPage.tsx +- frontend-v2/src/stores/appStore.ts + +### 验证 + +- npm.cmd run build 通过。 +- 构建产物已更新到 web/app-v2/。 +- 敏感关键词扫描仅命中说明性文案、脱敏正则和内存 access token 管理代码;未发现写死真实敏感值。 + +### 当前 /app-v2 已迁移页面 + +1. 总览 Dashboard。 +2. 服务器资产中心。 +3. 宝塔登录稳定性中心。 +4. 任务中心 / 自动化运行态。 +5. 审计日志 / 告警时间线。 +6. 安全巡检 / 风险总览。 +7. 系统设置 / 只读概览。 +8. 终端 / 文件安全入口。 +9. 执行记录只读中心。 + +### 仍回退旧后台的模块 + +- 真实 WebSSH 交互终端。 +- 文件上传、删除、写入、chmod、压缩/解压、跨服务器传输。 +- 脚本执行、停止、重试、完整日志和执行详情深查。 +- 宝塔凭据编辑和批量导入。 +- API Key 修改、SSH 私钥、白名单增删。 +- 会修改服务器状态或系统配置的深度修复动作。 + + +## 2026-07-07 继续推进:脚本库只读浏览 V2 + +本轮继续迁移第十个 V2 页面:脚本库只读浏览。 + +### 已新增 + +- 新增后端只读摘要接口:GET /api/scripts/summaries。 +- 该接口只返回脚本元数据,不返回 script.content,避免脚本正文进入 /app-v2 浏览器网络响应。 +- 新增 V2 页面:脚本库只读浏览。 +- 侧栏新增“脚本库”,进入 /app-v2 内部页面,不影响旧后台 /app/。 +- 页面使用 TanStack Query 缓存脚本摘要,使用 TanStack Table 渲染列表。 +- 支持按脚本名、分类、描述、创建者、ID 搜索。 +- 支持按 category 分类筛选。 +- 支持“只看有正文”筛选,但仅依据 has_content/content_size/line_count,不展示正文。 +- 指标卡展示脚本数量、分类数、正文规模和只读安全边界。 + +### 后端接口返回字段 + +GET /api/scripts/summaries 每条记录仅返回: + +- id +- name +- category +- description +- created_by +- created_at +- updated_at +- has_content +- content_size +- line_count + +明确不返回: + +- content +- 凭据 +- 执行参数 +- token/cookie/api key/private key 等敏感字段 + +### 安全边界 + +- V2 当前只读,不调用 /api/scripts/exec。 +- V2 当前不创建脚本、不编辑脚本、不删除脚本、不复制脚本正文。 +- V2 当前不返回 script.content,因此即使打开浏览器 DevTools 也不会从新接口看到脚本正文。 +- name/description 仍做前端二次脱敏,隐藏 password、token、cookie、secret、api key、private key、BT_PANEL、BT_KEY、Authorization、Bearer 等片段。 +- 编辑、复制正文、执行、删除、凭据选择继续跳回旧后台 /app/scripts。 +- script_service 作为管理员远程执行 shell 的预期功能继续保留;V2 只避免早期迁移阶段误触发写操作或暴露正文。 + +### 变更文件 + +- server/api/scripts.py +- frontend-v2/src/features/dashboard/components/ScriptLibraryPage.tsx +- frontend-v2/src/features/dashboard/api/dashboardApi.ts +- frontend-v2/src/features/dashboard/components/AppShell.tsx +- frontend-v2/src/features/dashboard/DashboardPage.tsx +- frontend-v2/src/stores/appStore.ts + +### 验证 + +- python -m py_compile server\api\scripts.py server\main.py 通过。 +- npm.cmd run build 通过。 +- 构建产物已更新到 web/app-v2/。 +- 搜索 /scripts 调用确认 V2 只使用 /api/scripts/summaries,不调用会返回正文的 /api/scripts/。 +- 敏感关键词扫描仅命中说明性文案、脱敏正则和内存 access token 管理代码;未发现写死真实敏感值。 + +### 当前 /app-v2 已迁移页面 + +1. 总览 Dashboard。 +2. 服务器资产中心。 +3. 宝塔登录稳定性中心。 +4. 任务中心 / 自动化运行态。 +5. 审计日志 / 告警时间线。 +6. 安全巡检 / 风险总览。 +7. 系统设置 / 只读概览。 +8. 终端 / 文件安全入口。 +9. 执行记录只读中心。 +10. 脚本库只读浏览。 + +### 仍回退旧后台的模块 + +- 真实 WebSSH 交互终端。 +- 文件上传、删除、写入、chmod、压缩/解压、跨服务器传输。 +- 脚本创建、编辑、删除、复制正文、执行、停止、重试、完整日志和执行详情深查。 +- 宝塔凭据编辑和批量导入。 +- API Key 修改、SSH 私钥、白名单增删。 +- 会修改服务器状态或系统配置的深度修复动作。 + +## 2026-07-07 继续推进:真实只读文件浏览 V2 + +本轮继续完善第二后台 `/app-v2` 的“终端 / 文件”能力,新增真实只读文件浏览第一步,目标是先把安全、低风险的目录浏览迁移到 V2,同时继续保留旧后台 `/app/files` 处理所有写操作和高风险操作。 + +### 已新增 + +- 新增 V2 组件:`frontend-v2/src/features/dashboard/components/ReadOnlyFileBrowser.tsx`。 +- 已接入到“终端 / 文件”页面:`frontend-v2/src/features/dashboard/components/TerminalFilesPage.tsx`。 +- 复用后端现有只读接口:`GET /api/files/browse?server_id=...&path=...`。 +- 新增前端 API 聚合类型与函数: + - `RemoteFileItem` + - `FileBrowseData` + - `fetchFileBrowseData(serverId, path)` +- 页面支持: + - 选择在线服务器; + - 输入远程绝对路径; + - 浏览目录; + - 点击目录进入下级; + - 返回上级目录; + - 刷新当前目录; + - 按名称、owner、group、权限过滤; + - 展示当前服务器、当前路径、目录/文件数量、文件体积合计。 + +### 安全边界 + +- V2 只调用 `GET /api/files/browse` 列目录。 +- V2 不读取文件正文。 +- V2 不上传文件。 +- V2 不删除文件。 +- V2 不写入文件。 +- V2 不 chmod/chown。 +- V2 不执行压缩、解压、跨服务器推送等会改变远程状态的动作。 +- 文件读取、上传、删除、chmod、压缩/解压、跨服务器推送仍跳回旧后台 `/app/files`。 +- 后端路径校验、SSH 执行、审计日志仍由现有 `server/api/files.py` 与 `files_browse_service.py` 负责。 + +### 本轮修复 + +- 修复 `ReadOnlyFileBrowser.tsx` 中路径正则写坏导致的 TypeScript/JSX 构建错误。 +- 清理未使用图标 import。 +- 重新用 UTF-8 写入该组件,避免中文文案和 JSX 标签出现乱码/截断问题。 + +### 变更文件 + +- `frontend-v2/src/features/dashboard/components/ReadOnlyFileBrowser.tsx` +- `frontend-v2/src/features/dashboard/components/TerminalFilesPage.tsx` +- `frontend-v2/src/features/dashboard/api/dashboardApi.ts` + +### 验证 + +- `npm.cmd run build` 通过。 +- 构建产物已更新到 `web/app-v2/`。 +- `python -m py_compile server\api\files.py server\api\scripts.py server\main.py` 通过。 +- 敏感关键词扫描仅命中说明性文案、脱敏正则和内存 access token 管理代码;未发现写死真实密码、Cookie、API Key、私钥、tmp_login token 等敏感值。 + +### 当前状态 + +- 本轮改动仍在本地快照中。 +- 尚未发布到 `https://api.synaglobal.vip`。 +- 旧后台 `/app/` 不受影响。 + +## 2026-07-08 继续推进:实时监控只读中心 V2 + +本轮继续完善第二后台 `/app-v2` 的“实时监控”只读能力,把监控槽、最近会话、探针记录和实时指标聚合到一个低风险页面中。旧后台 `/app/watch` 继续保留完整写操作能力,V2 当前只做查看与过滤。 + +### 已新增 / 已接入 + +- 新增 V2 组件:`frontend-v2/src/features/dashboard/components/WatchMonitoringPage.tsx`。 +- 新增模块枚举:`monitoring`。 +- `DashboardPage.tsx` 已接入 `WatchMonitoringPage`。 +- 前端 API 聚合新增: + - `WatchMetricPoint` + - `WatchProcessSnapshot` + - `WatchSlotItem` + - `WatchSessionItem` + - `WatchProbeRecordItem` + - `WatchMonitoringData` + - `fetchWatchMonitoringData(hours)` +- 页面支持 6h / 24h / 72h 时间窗口切换、刷新、监控槽卡片、CPU/内存/磁盘均值、sparkline、最近会话、最近探针记录、服务器/状态/错误过滤。 + +### 调用接口 + +- `GET /api/watch/pins`:读取监控槽。 +- `GET /api/watch/live?server_ids=...`:读取槽位服务器实时指标。 +- `GET /api/watch/sessions?hours=168`:读取最近监控会话。 +- `GET /api/watch/probe-records?hours=...&page=1&per_page=40`:读取最近探针记录。 + +### 安全边界 + +- V2 只调用 watch 模块 GET 接口。 +- V2 不创建监控槽。 +- V2 不删除或替换监控槽。 +- V2 不启停监控。 +- V2 不调整 TTL。 +- V2 不安装 psutil。 +- V2 不触发 SSH 写操作。 +- 监控槽编辑、安装依赖、启停监控、TTL 调整等写操作继续跳回旧后台 `/app/watch`。 + +### 变更文件 + +- `frontend-v2/src/features/dashboard/components/WatchMonitoringPage.tsx` +- `frontend-v2/src/features/dashboard/api/dashboardApi.ts` +- `frontend-v2/src/features/dashboard/components/AppShell.tsx` +- `frontend-v2/src/features/dashboard/DashboardPage.tsx` +- `frontend-v2/src/stores/appStore.ts` + +### 验证 + +- `npm.cmd run build` 通过。 +- `python -m py_compile server\api\watch.py server\main.py` 通过。 +- 敏感关键词扫描仅命中说明性文案、脱敏正则和内存 access token 管理代码;未发现写死真实敏感值。 + +### 当前状态 + +- 本地快照已更新。 +- 构建产物已更新到 `web/app-v2/`。 +- 尚未发布到 `https://api.synaglobal.vip`。 +- 旧后台 `/app/` 不受影响。 + + +## 2026-07-08 继续推进:SSH 会话 / 命令日志只读中心 V2 + +本轮继续迁移低风险只读模块,新增“SSH 会话 / 命令日志只读中心”。该页面用于审计查看最近 SSH 会话和命令日志,仍不提供 WebSSH、命令执行、会话回放、日志导出等写操作或高风险操作。 + +### 已新增 / 已接入 + +- 新增 V2 组件:`frontend-v2/src/features/dashboard/components/OperationLogsPage.tsx`。 +- 新增模块枚举:`operations`。 +- 侧栏新增“命令日志”入口。 +- `DashboardPage.tsx` 新增 `operations` 分发。 +- 同步补上上一轮遗漏的侧栏“实时监控”入口,确保 `monitoring` 页面可以从侧栏进入。 +- 前端 API 聚合新增: + - `OperationSshSessionItem` + - `OperationCommandLogItem` + - `OperationLogsData` + - `fetchOperationLogsData(serverId?)` + +### 调用接口 + +- `GET /api/assets/ssh-sessions?page=1&per_page=40`:读取 SSH 会话元数据。 +- `GET /api/assets/command-logs?page=1&per_page=120&redact=true`:读取命令日志摘要。 +- 可选追加 `server_id=...` 进行服务器筛选。 + +### 本轮后端安全增强 + +- `server/api/assets.py` 的 `GET /api/assets/command-logs` 新增 `redact: bool = Query(False)` 参数。 +- 当 `redact=true` 时,后端在返回前对命令中的敏感片段做服务端脱敏,包括: + - password / passwd / pwd + - token / cookie / secret + - api key / private key / access key + - BT_PANEL / BT_KEY / Authorization / Bearer + - `--password` / `--token` / `sshpass -p` / `-p...` 等常见命令参数。 +- 前端 `OperationLogsPage.tsx` 仍保留二次脱敏兜底,防止旧数据或异常格式直接展示。 + +### 安全边界 + +- V2 只调用 assets 模块 GET 接口。 +- V2 不打开 WebSSH。 +- V2 不执行命令。 +- V2 不重放命令。 +- V2 不复制未脱敏命令正文。 +- V2 不导出日志。 +- V2 不修改服务器状态、凭据、权限或审计配置。 +- 真实 WebSSH、命令执行、会话回放、导出和权限变更继续跳回旧后台 `/app/`。 + +### 变更文件 + +- `server/api/assets.py` +- `frontend-v2/src/features/dashboard/components/OperationLogsPage.tsx` +- `frontend-v2/src/features/dashboard/api/dashboardApi.ts` +- `frontend-v2/src/features/dashboard/components/AppShell.tsx` +- `frontend-v2/src/features/dashboard/DashboardPage.tsx` +- `frontend-v2/src/stores/appStore.ts` + +### 验证 + +- `npm.cmd run build` 通过,构建产物已更新到 `web/app-v2/`。 +- `python -m py_compile server\api\assets.py server\api\watch.py server\main.py` 通过。 +- 敏感关键词扫描仅命中脱敏规则、演示数据中的 `Authorization: [已脱敏]`、以及 `frontend-v2/src/lib/api.ts` 的内存 access token 管理;未发现真实敏感值。 + +### 当前状态 + +- 本轮改动仍在本地快照中。 +- 尚未发布到 `https://api.synaglobal.vip`。 +- 旧后台 `/app/` 不受影响。 + + +## 2026-07-08 继续推进:宝塔资源只读总览 V2 + +本轮继续迁移宝塔相关能力,新增“宝塔资源只读总览”。该页面用于集中查看单台宝塔服务器的站点、数据库、计划任务和系统资源快照。旧后台仍负责所有会改变远端状态的宝塔操作。 + +### 已新增 / 已接入 + +- 新增 V2 组件:`frontend-v2/src/features/dashboard/components/BtResourceOverviewPage.tsx`。 +- 新增模块枚举:`btresources`。 +- 侧栏新增“宝塔资源”入口。 +- `DashboardPage.tsx` 新增 `btresources` 分发。 +- 前端 API 聚合新增: + - `BtResourceRecord` + - `BtResourceOverviewData` + - `fetchBtResourceOverviewData(serverId?)` + +### 调用接口 + +V2 本轮只调用已有 GET 接口: + +- `GET /api/btpanel/servers/{server_id}/system/total` +- `GET /api/btpanel/servers/{server_id}/system/disk` +- `GET /api/btpanel/servers/{server_id}/system/network` +- `GET /api/btpanel/servers/{server_id}/sites` +- `GET /api/btpanel/servers/{server_id}/databases` +- `GET /api/btpanel/servers/{server_id}/crontab` + +旧后台的服务管理页当前主要是 `POST /api/btpanel/servers/{server_id}/services` 启停/重启服务,没有对应只读 GET 状态接口;因此 V2 本轮不做服务启停状态控制,只提供旧后台跳转。 + +### 页面能力 + +- 选择已配置宝塔的服务器。 +- 搜索站点 / 数据库 / 计划任务 / 备注。 +- 查看系统、宝塔版本、运行时间、CPU、内存、网络、磁盘快照。 +- 查看站点列表:名称、路径、启停状态、PHP、备注、创建时间。 +- 查看数据库列表:库名、用户、类型、备份数量、备注、创建时间。 +- 查看计划任务列表:任务名、类型、目标、启停状态、执行摘要、时间配置。 +- 计划任务命令摘要会做前端脱敏与截断,不展示完整敏感正文。 + +### 安全边界 + +- V2 只调用宝塔 GET 接口。 +- V2 不调用站点启停接口:`POST /sites/start`、`POST /sites/stop`。 +- V2 不创建站点:`POST /sites`。 +- V2 不新增/删除域名。 +- V2 不设置或申请 SSL。 +- V2 不创建数据库。 +- V2 不修改数据库密码。 +- V2 不触发数据库备份。 +- V2 不编辑或新增计划任务。 +- V2 不调用服务启停/重启接口:`POST /services`。 +- 所有会改变远端宝塔状态的操作继续回旧后台 `/app/btpanel/*` 执行。 + +### 变更文件 + +- `frontend-v2/src/features/dashboard/components/BtResourceOverviewPage.tsx` +- `frontend-v2/src/features/dashboard/api/dashboardApi.ts` +- `frontend-v2/src/features/dashboard/components/AppShell.tsx` +- `frontend-v2/src/features/dashboard/DashboardPage.tsx` +- `frontend-v2/src/stores/appStore.ts` + +### 验证 + +- `npm.cmd run build` 通过,构建产物已更新到 `web/app-v2/`。 +- `python -m py_compile server\api\btpanel.py server\api\assets.py server\main.py` 通过。 +- 敏感关键词扫描仅命中脱敏规则、演示脱敏文本、以及内存 access token 管理;未发现真实敏感值。 + +### 当前状态 + +- 本轮改动仍在本地快照中。 +- 尚未发布到 `https://api.synaglobal.vip`。 +- 旧后台 `/app/` 不受影响。 + +## 2026-07-08 继续推进:全局搜索 V2 只读中心 + +### 本次新增 + +- 新增 /app-v2 模块:全局搜索。 +- 侧栏新增入口:全局搜索。 +- 命令面板 Ctrl/Cmd + K 新增快捷项:打开全局搜索。 +- 新增页面文件:frontend-v2/src/features/dashboard/components/GlobalSearchPage.tsx +- 修改接入文件:frontend-v2/src/features/dashboard/api/dashboardApi.ts、frontend-v2/src/features/dashboard/components/AppShell.tsx、frontend-v2/src/features/dashboard/components/CommandPalette.tsx、frontend-v2/src/features/dashboard/DashboardPage.tsx、frontend-v2/src/stores/appStore.ts。 + +### 接口调用 + +- 只调用现有只读接口:GET /api/search/?q=... +- 后端 server/api/search.py 已做 LIKE 通配符 escape,避免搜索 % / _ 时扩大匹配范围。 +- V2 前端额外做输入标准化、至少 2 个字符才请求、350ms 防抖、TanStack Query 30 秒缓存。 +- 后端不可用时切换只读演示数据;如果后端未命中服务器但当前 Dashboard 资产缓存命中,则单独显示本地命中。 + +### 覆盖范围 + +1. 服务器:名称、域名、分组、在线状态。 +2. 脚本库:脚本名称、分类。 +3. 凭据元数据:名称、数据库类型、主机。 +4. 计划任务:计划名称、Cron 表达式、启停状态。 + +### 安全边界 + +- V2 搜索页只读,不新增/修改/删除任何业务对象。 +- 不展示真实密码、Cookie、Token、API Key、SSH 私钥、宝塔 tmp_login。 +- 凭据搜索只显示元数据;凭据编辑、密码查看/修改、私钥相关操作继续回旧后台。 +- 搜索结果点击目前跳回旧后台对应模块,避免 V2 早期误接高风险写操作。 + +### 验证结果 + +- 已通过前端构建:frontend-v2 下执行 npm.cmd run build。 +- 构建产物已更新到 web/app-v2/。 +- 已通过后端语法检查:python -m py_compile server/api/search.py server/main.py。 +- 敏感关键词扫描:命中均为脱敏规则、脱敏示例、说明文案或内存 access token 管理逻辑;未发现写入真实敏感值。 + + +## 2026-07-08 继续推进:一键登录并发边界、V2 脱敏、验证与 PowerShell 建议 + +### 本轮结论 + +- PowerShell 7+ 相比 Windows PowerShell 5.1 更适合作为 Nexus 本地开发 shell:默认 UTF-8 更稳定,跨平台命令行为更接近 Linux,`pwsh` 对中文输出/输入更友好。 +- 但 PowerShell 7+ 不是“彻底根治中文污染”的唯一手段:凡是涉及把中文脚本通过管道、heredoc / here-string 写入文件,仍建议使用 Node.js / Python 的 UTF-8 文件 API,或者直接使用 `cmd.exe` / Git Bash / WSL 执行构建测试。 +- 当前本地快照未发布到正式域名 `https://api.synaglobal.vip`。上线仍需要单独部署。 + +### PowerShell / Shell 使用建议 + +建议优先级: + +1. **文件写入 / 大段中文文档**:优先 Node.js `fs.writeFileSync(..., 'utf8')` 或 Python `Path.write_text(..., encoding='utf-8')`。 +2. **前端构建 / 后端编译 / ripgrep 扫描**:优先 `cmd.exe /c` 或 PowerShell 7 `pwsh -NoLogo -NoProfile -Command`。 +3. **SSH / Docker / Linux 批处理**:长期最好使用 WSL 或远端 Linux 主机执行,减少 Windows shell 差异。 +4. **避免 Windows PowerShell 5.1 管道写中文**,尤其不要使用: + + @' + 中文 Python 脚本 + '@ | python - + + 这类写法历史上已出现过把中文写成 `????` 的问题。 +5. 如果继续使用 Windows PowerShell 5.1,至少把执行链限制在“只读扫描/启动命令”,不要用它负责生成中文代码或中文文档。 + +### 宝塔一键登录并发修复复核 + +已确认本地快照中存在以下修复方向: + +- 同一服务器一键登录增加 Redis fail-fast 锁,避免双击、网关重试、多 worker 并发时后生成的 `tmp_token` 覆盖前一个 token。 +- 成功签发登录 URL 后增加短冷却 lease,减少 10 秒级连续打开多个宝塔页面时互相打掉登录态的概率。 +- 宝塔 bootstrap / TTL 修复增加 Redis 分布式锁,避免多进程同时改远端 `task.py`。 +- 远端 bootstrap 脚本改为 `mktemp + flock + os.replace()`,降低并发写坏远端文件的风险。 + +### API 相对 tmp_token 不依赖 SSH 的补丁 + +后端 `server/application/services/btpanel_service.py` 已补强: + +- 当宝塔 API 已返回 `/login?tmp_token=...` 或 `login_url` 时,优先使用已配置的 `BtPanelCredentials.base_url` 归一化 URL。 +- 不再因为“API 已成功但缺 SSH 凭据”而阻断本可成功的一键登录。 +- 这对只配置宝塔 API、没有 SSH 凭据的服务器更稳。 + +新增/保留的测试点: + +- `tests/test_btpanel_login_url.py` 增加“API 返回相对 tmp_token 时不调用 SSH”的测试用例。 + +### /app-v2 脱敏边界 + +本轮复核并保留以下策略: + +- V2 前端不显示全局 API Key。 +- V2 不展示宝塔密码、Cookie、tmp_login token、SSH 私钥、PEM 私钥、API Key 等敏感值。 +- 全局搜索对疑似敏感 query 直接阻断,不请求后端,不把原始敏感 query 放入 React Query cache key。 +- 文件浏览对敏感路径做脱敏/阻断,不把原始敏感路径放入 React Query cache key。 +- 后端错误信息进入 UI 前做 `safeWarningText()` 脱敏。 +- `bt.base_url` 显示前做 `sanitizeBtBaseUrl()`,避免把 URL 内用户名密码等信息带到页面。 +- credentials 搜索结果中的 host 做 `sanitizeHostDisplay()`,避免 DSN 形式 `user:pass@host` 泄漏。 + +### 本轮验证命令与结果 + +在本地快照目录执行: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 + +验证结果: + +1. 关键旧调用扫描: + + rg -n "resolve_temp_login_url" server\application\services\btpanel_service.py + + 结果:无命中,说明 `btpanel_service.py` 不再直接依赖旧的 SSH discovery 归一化函数。 + +2. 乱码扫描: + + rg -n "\\?{3,}|�" frontend-v2\src server tests + + 结果:无命中。 + +3. 全局 API Key 文案扫描: + + rg -n "全局 API Key|Global API Key" frontend-v2\src web\app-v2 + + 结果:无命中。 + +4. `api_key` 字段扫描: + + rg -n "api_key" frontend-v2\src web\app-v2 + + 结果:无命中。之前 fallback 演示设置里的空 `api_key` 字段名已移除。 + +5. 后端语法检查: + + python -m py_compile server\application\services\btpanel_service.py server\infrastructure\btpanel\bootstrap_lock.py server\infrastructure\btpanel\ssh_bootstrap.py tests\test_btpanel_login_url.py tests\test_btpanel_get_config.py tests\test_btpanel_bootstrap_lock.py tests\test_btpanel_ssh_bootstrap.py + + 结果:通过。 + +6. 前端构建: + + cd frontend-v2 + npm.cmd run build + + 结果:通过,构建产物已更新到 `web/app-v2/`。 + +7. pytest: + + python -m pytest tests\test_btpanel_ssh_bootstrap.py tests\test_btpanel_bootstrap_lock.py tests\test_btpanel_login_url.py tests\test_btpanel_get_config.py -q + + 结果:未执行成功,当前默认解释器为 `C:\Python314\python.exe`,缺少 pytest: + + C:\Python314\python.exe: No module named pytest + + 这属于本地测试环境问题,不是当前代码语法错误。建议后续用 Python 3.12 venv 固定依赖后再跑完整测试。 + +### 当前发布状态 + +- 本轮代码仍在本地快照目录。 +- 尚未确认已部署到 `https://api.synaglobal.vip`。 +- 如果要让正式站点看到 `/app-v2`、宝塔一键登录并发锁、API 相对 tmp_token 修复,需要执行正式发布流程。 + +### 后续待办 + +1. 建 Python 3.12 虚拟环境,安装 `pytest/sqlalchemy` 等 dev 依赖,跑完宝塔相关测试集。 +2. 给 `BootstrapRemoteError` 增加永久失败语义,避免 `ssh_no_credentials` / `ssh_sudo_required` 被错误当成可重试。 +3. 明确 Redis 锁异常策略:当前建议 Redis 对登录并发锁和 bootstrap 锁是 mandatory,Redis 不可用时宁可阻断,避免并发打坏 token 或远端文件。 +4. 给 `/app-v2` 增加构建后敏感词扫描脚本,并纳入 CI。 +5. 发布前再次扫描 `web/app-v2` 构建产物,确认无真实 token/key/私钥。 +6. 发布到正式环境后,在 `https://api.synaglobal.vip/app-v2/` 实测登录、列表、宝塔一键登录和旧后台跳转。 + + +## 2026-07-08 追加修复:BootstrapRemoteError 永久失败语义 + +### 处理原因 + +子审查发现:`BootstrapRemoteError` 之前只携带 `code`,`classify_ssh_result()` 虽然能判断 `ssh_no_credentials` / `ssh_sudo_required` / `ssh_auth_failed` 是否属于永久失败,但该布尔值没有传到 service 层,导致 service 层只能用 `exc.code == "ssh_auth_failed"` 粗略判断。 + +### 已修复 + +- `server/infrastructure/btpanel/ssh_bootstrap.py`: + - `BootstrapRemoteError` 新增 `permanent: bool = False` 属性。 + - `ssh_bootstrap_panel()` 在 SSH 执行失败时把 `classify_ssh_result()` 的 `permanent` 传入异常。 + - 修复了一处历史中文注释乱码。 +- `server/application/services/btpanel_service.py`: + - `bootstrap_server()` 改为使用 `exc.permanent` 写入 bootstrap 状态。 +- `tests/test_btpanel_ssh_bootstrap.py`: + - 增加 `test_ssh_bootstrap_panel_preserves_permanent_error_flag`,验证 sudo 需要密码会抛出 `permanent=True` 的 `BootstrapRemoteError`。 + +### 追加验证 + +- `python -m py_compile server\infrastructure\btpanel\ssh_bootstrap.py server\application\services\btpanel_service.py tests\test_btpanel_ssh_bootstrap.py`:通过。 +- 针对刚改文件扫描常见中文乱码片段:无命中。 + +### 待办更新 + +前一节“后续待办”中的 `BootstrapRemoteError` 永久失败语义已完成;剩余待办仍包括 Python 3.12 测试环境、Redis 异常策略测试/文档、构建产物敏感词 CI 扫描、正式发布验证。 + +## 2026-07-08 追加:/app-v2 构建产物敏感信息扫描脚本 + +### 处理原因 + +/app-v2 现在已经进入可并行使用阶段,发布前需要一个可以重复执行的检查,避免真实 API Key、Bearer token、JWT、宝塔 tmp_login、URL 内嵌账号密码、PEM 私钥等高风险内容被打进静态产物。 + +### 已新增文件 + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\frontend-v2\scripts\scan-app-v2-sensitive.mjs + +### 已更新文件 + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\frontend-v2\package.json + +新增 npm scripts: + + npm run scan:sensitive + npm run build:safe + +其中: + +- `scan:sensitive`:扫描 `../web/app-v2` 构建产物。 +- `build:safe`:先执行 TypeScript/Vite 构建,再执行敏感信息扫描。 + +### 当前扫描规则 + +扫描对象: + +- `.html` +- `.js` +- `.css` +- `.json` +- `.txt` +- `.svg` +- `.map` + +高置信阻断项: + +- “全局 API Key” / “Global API Key” 文案。 +- PEM 私钥块。 +- CSR/PEM 请求块。 +- JWT-like token。 +- `sk-...` 类型密钥。 +- GitHub token。 +- AWS Access Key ID。 +- Bearer token。 +- 宝塔 `tmp_login` / `tmp_token` 明文值。 +- URL 内嵌用户名密码。 +- 高置信 `password/api_key/secret/private_key/bt_key` 明文赋值。 + +### 验证结果 + +已在 `frontend-v2` 目录执行: + + npm.cmd run build:safe + +结果:通过。 + +关键输出: + + [app-v2-sensitive-scan] OK: scanned 9 file(s), no high-confidence secrets found in ...\web\app-v2 + +### 当前限制 + +- 这是高置信扫描,不替代完整 SAST/DAST。 +- 为避免误报,规则不会因为普通说明文字中出现 token/password 等词就失败。 +- 发布前仍建议配合后端接口脱敏、浏览器实际页面检查、以及线上响应头/缓存策略检查。 + +## 2026-07-08 追加:本地 Python 测试环境尝试结果 + +### 当前本机 Python 状态 + +执行: + + py -0p + +结果只有: + + -V:3.14 * C:\Python314\python.exe + +执行: + + python -m pip show pytest sqlalchemy + +结果:当前全局 Python 没有 pytest/sqlalchemy。 + +### 已尝试的隔离安装 + +已在项目快照目录创建临时虚拟环境: + + .venv-py314 + +第一次使用默认 PyPI 安装 `requirements.txt` + `requirements-dev.txt` 失败,原因是 PyPI SSL EOF。 + +随后切换清华 PyPI 镜像: + + .venv-py314\Scripts\python.exe -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt -r requirements-dev.txt + +下载依赖成功,但安装失败在 `pydantic-core==2.27.1`。根因:当前 Python 是 3.14,而该版本 pydantic-core 使用的 PyO3 最高支持到 Python 3.13。 + +又尝试设置: + + PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 + +继续安装,仍失败,后续错误来自 `jiter`/PyO3 与 Python 3.14 C API 不兼容。 + +### 结论 + +- 本地 Windows Python 3.14 不适合跑 Nexus 当前后端依赖。 +- 这不是 Nexus 代码语法问题,而是解释器版本与 pinned 依赖不兼容。 +- 后端单测需要 Python 3.12,或至少 Python 3.13;最推荐与生产 Docker 保持 Python 3.12。 + +### uv 检查 + +本机存在: + + C:\Users\uzuma\.local\bin\uv.exe + +但 `uv python list --only-installed` 默认读取用户 uv cache / install 目录时报权限或路径问题。即使设置工作区 `UV_CACHE_DIR` 与 `UV_PYTHON_INSTALL_DIR`,仍会扫到不可访问的 `C:\Users\uzuma\.local\bin\python3.14.exe`。 + +### 下一步建议 + +1. 在 Windows 安装 Python 3.12 后重新建 `.venv-py312`。 +2. 或直接用 Docker Python 3.12 镜像跑 pytest。 +3. 或在局域网 Linux 测试部署机上安装 Python 3.12/venv 后跑后端测试。 +4. 当前已能继续做 `py_compile`、前端构建、V2 敏感产物扫描;完整 pytest 暂时不能在本机 Python 3.14 上完成。 + +## 2026-07-08 追加:后端测试环境检查脚本 + +### 已新增文件 + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\check_backend_test_env.py + +### 用途 + +在新机器或本机执行 pytest 前,先检查当前 Python 是否适合跑 Nexus 后端测试。 + +检查项: + +- Python 版本必须是 3.12 或 3.13。 +- 明确阻止 Python 3.14,因为当前 pinned `pydantic-core` / PyO3 不兼容。 +- 检查 `pytest`、`sqlalchemy`、`pydantic`、`fastapi` 是否可 import。 + +### 当前验证 + +在本机执行: + + python scripts\check_backend_test_env.py + +结果:按预期失败并提示需要 Python 3.12/3.13。 + +关键输出: + + Python executable: C:\Python314\python.exe + Python version: 3.14.4 + ERROR: Nexus backend tests should be run with Python 3.12 or 3.13. + +这个脚本后续可以放到本地测试说明或 CI 前置检查中,避免在不兼容解释器上浪费依赖安装时间。 + +## 2026-07-08 追加:本地发布前 Gate 与宝塔测试入口 + +### 已新增文件 + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\run_local_release_gate.py + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\run_btpanel_tests.py + +### run_local_release_gate.py + +用途:发布前在本地快照上跑一组稳定、可重复的检查。 + +默认执行: + +1. `python -m py_compile` 检查本轮关键后端、测试、发布脚本。 +2. `frontend-v2` 下执行 `npm run build:safe`。 +3. `build:safe` 内部包含 TypeScript 检查、Vite 构建、`web/app-v2` 高置信敏感信息扫描。 +4. 默认跳过 pytest,因为当前本机 Python 3.14 不适合跑 pinned 后端依赖。 + +执行命令: + + python scripts\run_local_release_gate.py + +当前验证结果:通过。 + +关键输出: + + [app-v2-sensitive-scan] OK: scanned 9 file(s), no high-confidence secrets found + [release-gate] pytest skipped. Use --with-pytest under Python 3.12/3.13 to run BtPanel tests. + [release-gate] OK + +如在 Python 3.12/3.13 的正确测试环境里,可以执行: + + python scripts\run_local_release_gate.py --with-pytest + +### run_btpanel_tests.py + +用途:集中运行宝塔相关后端测试,避免每次手工拼测试文件列表。 + +默认覆盖测试文件: + +- `tests/test_btpanel_bootstrap_lock.py` +- `tests/test_btpanel_bootstrap_loop.py` +- `tests/test_btpanel_client.py` +- `tests/test_btpanel_file_urls.py` +- `tests/test_btpanel_get_config.py` +- `tests/test_btpanel_login_url.py` +- `tests/test_btpanel_login_url_route.py` +- `tests/test_btpanel_server_list.py` +- `tests/test_btpanel_ssh_bootstrap.py` +- `tests/test_btpanel_ssh_login.py` +- `tests/test_btpanel_temp_login_ttl.py` + +执行命令: + + python scripts\run_btpanel_tests.py + +当前本机验证:按预期被 `scripts/check_backend_test_env.py` 阻断,因为当前是 Python 3.14。 + +关键输出: + + ERROR: Nexus backend tests should be run with Python 3.12 or 3.13. + Python executable: C:\Python314\python.exe + Python version: 3.14.4 + +### 当前发布建议 + +- 本地每次准备发布 `/app-v2` 或宝塔登录相关改动前,先跑 `python scripts\run_local_release_gate.py`。 +- 在 Python 3.12/3.13、Docker、WSL 或局域网 Linux 测试机里,再跑 `python scripts\run_local_release_gate.py --with-pytest`。 +- 正式部署到 `https://api.synaglobal.vip` 后,还需要线上实际验证 `/app-v2/`、宝塔一键登录、多个宝塔并发打开、2 小时后刷新保持登录、新增宝塔 hardcoded_3600 兜底修复。 + + +## 2026-07-08 追加:本轮继续验证与 Docker 测试入口补强 + +### 本轮完成事项 + +1. 已把宝塔一键登录 API 路由的并发锁超时测试加入发布前 Gate。 +2. 已检查新增 Docker 测试脚本、宝塔路由测试脚本、发布 Gate 脚本,没有发现中文乱码占位符。 +3. 已重新执行本地发布前 Gate,结果通过。 +4. 已为 Docker Python 3.12 宝塔测试入口加入镜像拉取代理环境变量兜底,默认使用用户指定的 socks5 代理:`socks5://127.0.0.1:10808`。 +5. 已尝试执行 Docker Python 3.12 宝塔测试入口;当前 Windows 环境未安装或不可用 `docker` 命令,因此 Docker pytest 未能在本机完成。 + +### 本轮修改文件 + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\run_local_release_gate.py + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\run_btpanel_tests_docker.cmd + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\run_btpanel_tests_docker.sh + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\tests\test_btpanel_login_url_route.py + +### 发布前 Gate 当前覆盖 + +`scripts/run_local_release_gate.py` 当前会编译检查以下关键文件: + +- `server/application/services/btpanel_service.py` +- `server/infrastructure/btpanel/bootstrap_lock.py` +- `server/infrastructure/btpanel/login_url_lock.py` +- `server/infrastructure/btpanel/ssh_bootstrap.py` +- `tests/test_btpanel_bootstrap_lock.py` +- `tests/test_btpanel_get_config.py` +- `tests/test_btpanel_login_url.py` +- `tests/test_btpanel_login_url_route.py` +- `tests/test_btpanel_ssh_bootstrap.py` +- `scripts/check_backend_test_env.py` +- `scripts/run_btpanel_tests.py` +- `scripts/run_local_release_gate.py` + +其中 `tests/test_btpanel_login_url_route.py` 覆盖:同一宝塔一键登录 URL 正在签发时,`LoginUrlLockTimeout` 应映射为 HTTP 409,而不是返回 500 或误判成功。 + +### 本轮实际验证结果 + +执行: + + python -m py_compile tests\test_btpanel_login_url_route.py scripts\run_btpanel_tests.py scripts\run_local_release_gate.py scripts\check_backend_test_env.py + +结果:通过。 + +执行乱码扫描: + + tests/test_btpanel_login_url_route.py + scripts/run_btpanel_tests_docker.cmd + scripts/run_btpanel_tests_docker.sh + scripts/run_btpanel_tests.py + scripts/run_local_release_gate.py + +结果: + + NO_BAD_TEXT + +执行本地发布前 Gate: + + python scripts\run_local_release_gate.py + +结果:通过。关键输出: + + tests/test_btpanel_login_url_route.py 已包含在 py_compile 目标内 + [app-v2-sensitive-scan] OK: scanned 9 file(s), no high-confidence secrets found + [release-gate] pytest skipped. Use --with-pytest under Python 3.12/3.13 to run BtPanel tests. + [release-gate] OK + +执行 Docker Python 3.12 宝塔测试入口: + + scripts\run_btpanel_tests_docker.cmd + +当前结果: + + ERROR: docker is not available + +结论:Docker 测试入口脚本本身已就绪,但当前 Windows 环境没有可调用的 Docker CLI;这属于本机环境缺口,不是 Nexus 代码测试失败。 + +### Docker 测试入口的代理行为 + +新增环境变量: + + NEXUS_DOCKER_PULL_PROXY + +默认值: + + socks5://127.0.0.1:10808 + +如果不想走代理,可以执行前设置: + +Windows CMD: + + set NEXUS_DOCKER_PULL_PROXY=off + scripts\run_btpanel_tests_docker.cmd + +Linux / WSL: + + NEXUS_DOCKER_PULL_PROXY=off bash scripts/run_btpanel_tests_docker.sh + +如果当前 Docker Desktop / Docker daemon 不读取 CLI 环境变量,则还需要在 Docker daemon 或 Docker Desktop 的代理设置里配置同一个 socks5 代理。 + +### PowerShell 7+ 建议 + +PowerShell 7+ 对 UTF-8、Node、Python、Docker、SSH 更友好,建议安装并作为 Windows 本地默认 shell 使用。但 Nexus 的构建、pytest、Docker、批量 SSH 更推荐放到 WSL 或局域网 Linux 测试部署机执行。 + +稳定策略: + +1. Windows 本地用 PowerShell 7+ / CMD 执行轻量命令。 +2. 写中文 Markdown、批量改代码优先用 Node/Python UTF-8 API,不用 PowerShell 5.1 管道写文件。 +3. 后端 pytest 使用 Python 3.12/3.13、Docker Python 3.12、WSL 或局域网 Linux 测试机,不使用本机 Python 3.14。 + +### 下一步 + +1. 在 Docker 可用的环境里执行: + + scripts\run_btpanel_tests_docker.cmd + + 或: + + bash scripts/run_btpanel_tests_docker.sh + +2. 如果仍在 Windows 本机执行,需要先安装 Docker Desktop 或改用 WSL/Linux 测试部署机。 +3. Docker pytest 通过后,再发布到正式环境 `https://api.synaglobal.vip`。 +4. 发布后线上验证:`/app-v2/`、宝塔一键登录、并发打开同一宝塔返回 409 兜底、多个宝塔 2 小时后刷新不掉线、新增宝塔自动检测/修复 hardcoded_3600。 + +## 2026-07-08 追加:Python 3.12 完整发布前 Gate 已跑通 + +### 本轮结论 + +已不再依赖本机 Python 3.14,也不再被本机 Docker 不可用卡住。使用 Codex bundled Python 3.12.13 创建了独立虚拟环境,并成功跑完整发布前 Gate: + + py_compile + /app-v2 build:safe + 宝塔相关 pytest + +最终结果: + + [release-gate] OK + 60 passed in 6.73s + +### 新增 / 修改点 + +#### 1. Python 3.12 测试虚拟环境 + +位置: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\.venv-py312-codex + +Python 版本: + + Python 3.12.13 + +依赖已安装: + + requirements.txt + requirements-dev.txt + +#### 2. Windows 测试环境补充 tzdata + +文件: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\requirements-dev.txt + +新增: + + tzdata>=2025.2 + +原因: + +Windows Python 环境没有系统 IANA 时区库,`ZoneInfo("Asia/Shanghai")` 会报错。Nexus 测试导入 `server.utils.display_time` 时需要这个时区数据。 + +#### 3. server/main.py 支持 Windows host-side 测试 + +文件: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\server\main.py + +变更: + +- Linux / 生产环境继续使用 `fcntl.flock`。 +- Windows 测试环境没有 `fcntl`,增加 host-side fallback,避免导入 `server.main` 时直接失败。 +- 该 fallback 只用于 Windows 本地测试兼容;生产 Linux 仍走原来的非阻塞 flock。 + +#### 4. 宝塔测试入口参数转发修复 + +文件: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\run_btpanel_tests.py + +修复: + +- 支持向 pytest 透传参数。 +- 示例: + + python scripts\run_btpanel_tests.py -- -q + +#### 5. SSH bootstrap 测试导入修复 + +文件: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\tests\test_btpanel_ssh_bootstrap.py + +修复: + +- 新增 `ssh_bootstrap_module` 导入,保证 `monkeypatch.setattr(...)` 能正确 patch `exec_ssh_command`。 +- 新增 `ssh_bootstrap_panel` 导入,覆盖 `BootstrapRemoteError.permanent` 透传测试。 + +#### 6. release gate 覆盖增强 + +文件: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\run_local_release_gate.py + +新增 py_compile 覆盖: + + server/main.py + +这样 Windows `fcntl` fallback 和静态挂载入口也进入发布前语法检查。 + +### 实际执行命令 + +创建 Python 3.12 venv: + + C:\Users\uzuma\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe -m venv .venv-py312-codex + +安装依赖: + + .venv-py312-codex\Scripts\python.exe -m pip install -r requirements.txt -r requirements-dev.txt + +单独跑宝塔测试: + + .venv-py312-codex\Scripts\python.exe scripts\run_btpanel_tests.py -- -q + +结果: + + 60 passed in 1.70s + +完整发布前 Gate: + + .venv-py312-codex\Scripts\python.exe scripts\run_local_release_gate.py --with-pytest + +关键输出: + + [app-v2-sensitive-scan] OK: scanned 9 file(s), no high-confidence secrets found + collected 60 items + 60 passed in 6.73s + [release-gate] OK + +### 已覆盖的宝塔测试文件 + +- `tests/test_btpanel_bootstrap_lock.py` +- `tests/test_btpanel_bootstrap_loop.py` +- `tests/test_btpanel_client.py` +- `tests/test_btpanel_file_urls.py` +- `tests/test_btpanel_get_config.py` +- `tests/test_btpanel_login_url.py` +- `tests/test_btpanel_login_url_route.py` +- `tests/test_btpanel_server_list.py` +- `tests/test_btpanel_ssh_bootstrap.py` +- `tests/test_btpanel_ssh_login.py` +- `tests/test_btpanel_temp_login_ttl.py` + +### 本轮额外安全检查 + +已检查本轮改动文件: + + server/main.py + requirements-dev.txt + scripts/run_local_release_gate.py + scripts/run_btpanel_tests.py + tests/test_btpanel_ssh_bootstrap.py + +检查结果: + + NO_BAD_TEXT_OR_SECRET + +也确认之前因 Windows CMD `>` 语义误生成的临时文件已删除: + + redirect_file_exists False + +### 当前发布判断 + +本地快照层面,宝塔一键登录相关代码已经具备更强发布前证据: + +- Python 3.12 环境检查通过。 +- 宝塔 60 个 pytest 全部通过。 +- `/app-v2` 构建通过。 +- `/app-v2` 构建产物敏感信息扫描通过。 +- release gate 已包含 pytest,最终 `[release-gate] OK`。 + +下一步可以进入发布环节,但发布到 `https://api.synaglobal.vip` 后仍需要线上实际验证: + +1. `/app-v2/` 是否可访问。 +2. 宝塔一键登录是否成功。 +3. 同一台宝塔短时间重复点击是否返回 409 兜底。 +4. 同时打开多个宝塔后,2 小时刷新是否不再陆续掉线。 +5. 新增宝塔是否自动检测并修复 hardcoded_3600。 + +## 2026-07-08 追加:发布差异清单与发布前清单 + +### 新增文档 + +差异清单: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md + +发布前清单: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-ready-checklist-20260708.md + +### 当前差异统计 + +已排除 `.venv-*`、`node_modules`、`.pytest_cache`、`__pycache__`、`*.bak-`、`2025.2`、`=2025.2`、`tmp/nexus-frontend.tar.gz` 等不应发布内容后: + + added=381 + modified=26 + deleted=347 + +### 发布候选安全扫描 + +执行: + + .venv-py312-codex\Scripts\python.exe scripts\scan_release_manifest.py --root C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --manifest C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md + +结果: + + OK: no known environment leak markers found in release manifest entries + +### 最新完整 Gate + +执行: + + .venv-py312-codex\Scripts\python.exe scripts\run_local_release_gate.py --with-pytest + +结果: + + [app-v2-sensitive-scan] OK: scanned 9 file(s), no high-confidence secrets found + 60 passed in 1.79s + [release-gate] OK + +### 干净发布包 + +已生成并扫描: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz + +扫描结果: + + size_mb=27.02 + entries=2409 + OK: release bundle looks clean + +本轮还修复了 `.dockerignore` 与 `deploy/rsync-local-to-server.sh`,避免发布时同步 `frontend-v2/node_modules`、`.venv-*`、`.pytest_cache`、`2025.2`、`*.bak-` 等脏文件。 + +### 2026-07-08 追加:部署脚本和发布后验证补强 + +新增: + + scripts/check_deploy_excludes.py + scripts/verify_online_release.py + +发布前 Gate 已加入: + + bash -n deploy/deploy-production.sh + bash -n deploy/rsync-local-to-server.sh + python scripts/check_deploy_excludes.py + +`deploy/deploy-production.sh` 发布后健康检查已增加 `/app-v2/`,不再只检查旧 `/app/`。 + +发布后可执行: + + .venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip + +本轮尝试线上 SSH 只读探测时,Codex 审批系统返回 404 并拒绝执行;因此尚未实际发布线上。 + +### 2026-07-08 追加:/app-v2 发布同步路径补强 + +已补强: + + deploy/deploy-production.sh + deploy/sync_webapp_to_container.sh + scripts/run_local_release_gate.py + +变化: + +- `deploy/deploy-production.sh` 现在会显式执行 `frontend-v2` 的 `npm run build:safe`。 +- 发布脚本会把 `web/app-v2/index.html` 与 `web/app-v2/assets/` 打包上传到宿主机。 +- Docker 运行时的 `sync_webapp_to_container.sh` 会同步 `web/app-v2` 到容器 `/app/web/app-v2`。 +- 容器同步脚本会校验 `/app-v2/` 主 bundle 是否 HTTP 200。 +- release gate 已加入 `bash -n deploy/sync_webapp_to_container.sh`。 + +最新验证: + + 60 passed in 1.70s + [release-gate] OK + +发布包重新生成并扫描: + + files=2409 + entries=2409 + OK: release bundle looks clean + +### 2026-07-08 追加:发布包 SHA256 与手动部署命令 + +发布包: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz + +SHA256: + + 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b + +元数据: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.metadata.json + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-artifact-metadata-20260708.md + +手动部署命令: + + C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-manual-deploy-commands-20260708.md + +最新 Gate: + + 60 passed in 1.70s + [release-gate] OK + +### 2026-07-08 追加:服务器侧发布包应用脚本 + +新增: + + deploy/apply-release-bundle.sh + +用途: + +- 在线上服务器校验发布包 SHA256。 +- 校验发布包必需路径和禁止路径。 +- 自动备份当前部署目录。 +- rsync 覆盖代码但保留 `.env`、`docker/.env.prod`、`web/data`。 +- Docker 模式下执行 `nexus-1panel.sh upgrade --skip-git`。 +- 同步 `/app/` 与 `/app-v2/` 到容器。 +- 验证本机 `/health`、`/app/`、`/app-v2/`。 + +最新发布包: + + entries=2409 + SHA256=46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b + + +## 2026-07-08 validate-only verification + +The server-side release applier now supports validate-only mode. + +Command verified locally with Git Bash: + +```bash +bash deploy/apply-release-bundle.sh --validate-only --tarball /c/Users/uzuma/Documents/Codex/2026-07-06/yuu/outputs/nexus-release-20260708.tar.gz --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +` + +Result: + +` ext +SHA256 OK: 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +Extracted release tree validation OK +Validate-only mode: tarball checksum and content validation passed +` + +Network/SSH production execution is still not performed because Codex approval auto-review returned HTTP 404 for read-only checks. + + +## 2026-07-08 one-command local artifact preparation + +A single local command now prepares and verifies the release artifact end-to-end: + +`cmd +.venv-py312-codex\Scripts\python.exe scripts\prepare_release_artifact.py --old-snapshot C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --output-dir C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs --name nexus-release-20260708 +` + +Verified result: + +` ext +[prepare-release-artifact] OK +added=381 modified=26 deleted=347 +files=2409 +entries=2409 +size_mb=27.02 +sha256=46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +60 passed in 1.70s +` diff --git a/docs/release/20260708/Nexus-manual-deploy-commands-20260708.md b/docs/release/20260708/Nexus-manual-deploy-commands-20260708.md new file mode 100644 index 00000000..ea8bb1c1 --- /dev/null +++ b/docs/release/20260708/Nexus-manual-deploy-commands-20260708.md @@ -0,0 +1,236 @@ +# Nexus 手动发布命令清单(2026-07-08) + +> 适用场景:Codex 远程 SSH 审批不可用时,由人工在本机和线上服务器执行。 +> 发布包已通过本地 Gate 和发布包扫描。 + +## 本地发布包 + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +SHA256: + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +## 方案 A:使用现有 deploy/deploy-production.sh(推荐) + +前提: + +- 本机能通过 SSH 访问线上 Host:`nexus` +- 或设置 `NEXUS_SSH=user@host` +- 线上已有 `/opt/nexus` 或 `/www/wwwroot/api.synaglobal.vip` + +在源码快照目录执行: + +```bash +cd /c/Users/uzuma/Documents/Codex/2026-07-06/yuu/work/nexus_code_context/snapshots/nexus-source-20260707-112042 +bash deploy/deploy-production.sh +``` + +如果不用 SSH config: + +```bash +export NEXUS_SSH='root@你的线上服务器IP' +export NEXUS_DEPLOY_PATH='/opt/nexus' +bash deploy/deploy-production.sh +``` + +发布脚本会: + +1. 本地同步源码到线上。 +2. 排除 `.venv-*`、`node_modules`、缓存、误生成文件。 +3. Docker 模式下执行 `nexus-1panel.sh upgrade --skip-git`。 +4. 构建并上传旧 `/app/`。 +5. 构建并上传新 `/app-v2/`。 +6. Docker 模式下同步 `web/app` 与 `web/app-v2` 到容器。 +7. 检查 `/health`、`/app/`、`/app-v2/`。 + +## 方案 B:发布 tar.gz 包到线上 + +### 1. 上传 + +Windows CMD / PowerShell: + +```cmd +scp C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz nexus:/tmp/nexus-release-20260708.tar.gz +``` + +### 2. 线上校验 + +```bash +cd /tmp +sha256sum nexus-release-20260708.tar.gz +``` + +必须等于: + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +### 3. 线上备份 + +假设部署目录为 `/opt/nexus`: + +```bash +sudo mkdir -p /opt/nexus-backups +sudo tar czf /opt/nexus-backups/nexus-before-20260708.tar.gz -C /opt nexus +``` + +### 3B. 使用服务器侧脚本自动应用发布包(推荐) + +如果线上已存在当前项目的 `deploy/apply-release-bundle.sh`,或者你先把发布包解开到临时目录后使用新脚本,可以执行: + +```bash +sudo bash /opt/nexus/deploy/apply-release-bundle.sh \ + --tarball /tmp/nexus-release-20260708.tar.gz \ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \ + --deploy-path /opt/nexus +``` + +如果脚本尚未在旧线上目录中,可以先临时解出脚本: + +```bash +cd /tmp +tar xzf nexus-release-20260708.tar.gz nexus-release/deploy/apply-release-bundle.sh +sudo bash /tmp/nexus-release/deploy/apply-release-bundle.sh \ + --tarball /tmp/nexus-release-20260708.tar.gz \ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \ + --deploy-path /opt/nexus +``` + +这个脚本会自动: + +1. 校验 SHA256。 +2. 检查发布包内必须路径和禁止路径。 +3. 备份当前 `/opt/nexus`。 +4. rsync 覆盖代码,但保留 `.env`、`docker/.env.prod`、`web/data`。 +5. Docker 模式下执行 `nexus-1panel.sh upgrade --skip-git`。 +6. 同步 `/app/` 和 `/app-v2/` 静态资源到容器。 +7. 验证本机 `/health`、`/app/`、`/app-v2/`。 + +### 4. 解包覆盖 + +```bash +sudo mkdir -p /opt/nexus +sudo tar xzf /tmp/nexus-release-20260708.tar.gz -C /tmp +sudo rsync -a --delete \ + --exclude='.env' \ + --exclude='docker/.env.prod' \ + --exclude='web/data' \ + /tmp/nexus-release/ /opt/nexus/ +``` + +### 5. 重建/重启 + +Docker/1Panel 模式: + +```bash +cd /opt/nexus +sudo env NEXUS_ROOT=/opt/nexus bash deploy/nexus-1panel.sh upgrade --skip-git +``` + +如果是 Supervisor 模式: + +```bash +sudo supervisorctl restart nexus +``` + +### 6. Docker 容器内静态资源同步 + +Docker 模式建议执行: + +```bash +cd /opt/nexus +sudo env NEXUS_ROOT=/opt/nexus bash deploy/sync_webapp_to_container.sh +``` + + +## 方案 C:Python 发布辅助脚本(推荐替代手工 scp/ssh) + +默认只上传并执行服务器侧 preflight,不会正式 apply: + +```cmd +cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 +.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^ + --remote nexus ^ + --tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +preflight 输出确认无误后,再加 `--apply` 正式发布: + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^ + --remote nexus ^ + --tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^ + --apply +``` + +详见:`Nexus-release-python-publisher-20260708.md`。 + +## 发布后验证 + +本机执行: + +```cmd +cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 +.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip +``` + +线上执行: + +```bash +curl -s https://api.synaglobal.vip/health +curl -I https://api.synaglobal.vip/app/ +curl -I https://api.synaglobal.vip/app-v2/ +``` + +期望: + +```text +/health => ok +/app/ => HTTP 200 +/app-v2/ => HTTP 200 +``` + +## 宝塔专项验证 + +发布后在 Nexus 后台验证: + +1. 打开 `/app/` 旧后台,确认宝塔列表正常。 +2. 打开 `/app-v2/` 新后台,确认页面可访问。 +3. 对同一台宝塔连续点击一键登录,冲突时应该返回 HTTP 409。 +4. 对新增宝塔执行一键登录,应触发 hardcoded_3600 检测/修复兜底。 +5. 多台宝塔同时打开后,2 小时刷新不应因为 `task.py` hardcoded `3600` 陆续掉线。 + +## 回滚 + +如果异常: + +```bash +cd /opt +sudo mv nexus nexus-bad-20260708 +sudo tar xzf /opt/nexus-backups/nexus-before-20260708.tar.gz -C /opt +cd /opt/nexus +sudo env NEXUS_ROOT=/opt/nexus bash deploy/nexus-1panel.sh upgrade --skip-git +``` + + + +## Server preflight command + +Before applying the release on the production host, run: + +```bash +bash /tmp/nexus-release/deploy/preflight-release-host.sh \ + --tarball /tmp/nexus-release-20260708.tar.gz \ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \ + --deploy-path /opt/nexus +``` + +This checks required commands, checksum, release tar paths, deploy path, runtime hints, disk space, and current local endpoints before applying changes. diff --git a/docs/release/20260708/Nexus-release-artifact-metadata-20260708.md b/docs/release/20260708/Nexus-release-artifact-metadata-20260708.md new file mode 100644 index 00000000..2635bacd --- /dev/null +++ b/docs/release/20260708/Nexus-release-artifact-metadata-20260708.md @@ -0,0 +1,21 @@ +# Nexus release artifact metadata + +- File: `nexus-release-20260708.tar.gz` +- Size: `27.02 MB` +- Entries: `2409` +- SHA256: `46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b` +- Created UTC: `2026-07-08T09:59:20.278597+00:00` + +## Verify after upload + +Linux: + +```bash +sha256sum nexus-release-20260708.tar.gz +``` + +PowerShell: + +```powershell +Get-FileHash .\nexus-release-20260708.tar.gz -Algorithm SHA256 +``` diff --git a/docs/release/20260708/Nexus-release-diff-manifest-20260708.md b/docs/release/20260708/Nexus-release-diff-manifest-20260708.md new file mode 100644 index 00000000..d48623c5 --- /dev/null +++ b/docs/release/20260708/Nexus-release-diff-manifest-20260708.md @@ -0,0 +1,777 @@ +# Nexus release diff manifest (2026-07-08) + +Baseline: + +- Old snapshot: `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236` +- New snapshot: `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042` + +Excluded names/patterns: `.git`, `node_modules`, `.venv-*`, `venv`, `__pycache__`, `.ruff_cache`, `.playwright-mcp`, `.cursor`, `.pytest_cache`, `tmp`, `*.pyc`, `*.pyo`, `*.bak-`, `2025.2`, `=2025.2`. + +## Summary + +- Added files: 381 +- Modified files: 26 +- Deleted files: 347 + +## Added files + +- `.skills/nexus-btpanel-review/SKILL.md` +- `.skills/nexus-security-review/SKILL.md` +- `.skills/nexus-ssh-safety/SKILL.md` +- `deploy/apply-release-bundle.sh` +- `deploy/preflight-release-host.sh` +- `docs/security/Nexus-SSH鍛戒护鎵ц瀹夊叏宸℃闃舵3-鏂囦欢浼犺緭涓庝笓鐢⊿kill-20260707.md` +- `frontend-v2/index.html` +- `frontend-v2/package-lock.json` +- `frontend-v2/package.json` +- `frontend-v2/postcss.config.js` +- `frontend-v2/scripts/scan-app-v2-sensitive.mjs` +- `frontend-v2/src/App.tsx` +- `frontend-v2/src/components/ui/Badge.tsx` +- `frontend-v2/src/components/ui/Panel.tsx` +- `frontend-v2/src/features/dashboard/DashboardPage.tsx` +- `frontend-v2/src/features/dashboard/api/dashboardApi.ts` +- `frontend-v2/src/features/dashboard/components/AppShell.tsx` +- `frontend-v2/src/features/dashboard/components/AssetInventoryPage.tsx` +- `frontend-v2/src/features/dashboard/components/AuditLogPage.tsx` +- `frontend-v2/src/features/dashboard/components/AuditTimeline.tsx` +- `frontend-v2/src/features/dashboard/components/BtDomainSslPage.tsx` +- `frontend-v2/src/features/dashboard/components/BtPanelPage.tsx` +- `frontend-v2/src/features/dashboard/components/BtResourceOverviewPage.tsx` +- `frontend-v2/src/features/dashboard/components/CommandPalette.tsx` +- `frontend-v2/src/features/dashboard/components/ExecutionRecordsPage.tsx` +- `frontend-v2/src/features/dashboard/components/GlobalSearchPage.tsx` +- `frontend-v2/src/features/dashboard/components/HealthScore.tsx` +- `frontend-v2/src/features/dashboard/components/OperationLogsPage.tsx` +- `frontend-v2/src/features/dashboard/components/ReadOnlyFileBrowser.tsx` +- `frontend-v2/src/features/dashboard/components/ScriptLibraryPage.tsx` +- `frontend-v2/src/features/dashboard/components/SecurityInspectionPage.tsx` +- `frontend-v2/src/features/dashboard/components/ServerDrawer.tsx` +- `frontend-v2/src/features/dashboard/components/ServerTable.tsx` +- `frontend-v2/src/features/dashboard/components/SettingsOverviewPage.tsx` +- `frontend-v2/src/features/dashboard/components/StatCard.tsx` +- `frontend-v2/src/features/dashboard/components/TaskCenter.tsx` +- `frontend-v2/src/features/dashboard/components/TaskCenterPage.tsx` +- `frontend-v2/src/features/dashboard/components/TerminalFilesPage.tsx` +- `frontend-v2/src/features/dashboard/components/TrendPanel.tsx` +- `frontend-v2/src/features/dashboard/components/TtlMatrix.tsx` +- `frontend-v2/src/features/dashboard/components/WatchMonitoringPage.tsx` +- `frontend-v2/src/features/dashboard/data/dashboardData.ts` +- `frontend-v2/src/features/dashboard/hooks/useDashboardData.ts` +- `frontend-v2/src/lib/api.ts` +- `frontend-v2/src/lib/redaction.ts` +- `frontend-v2/src/lib/utils.ts` +- `frontend-v2/src/main.tsx` +- `frontend-v2/src/stores/appStore.ts` +- `frontend-v2/src/styles.css` +- `frontend-v2/src/vite-env.d.ts` +- `frontend-v2/tailwind.config.ts` +- `frontend-v2/tsconfig.json` +- `frontend-v2/tsconfig.node.json` +- `frontend-v2/vite.config.ts` +- `scripts/check_backend_test_env.py` +- `scripts/check_deploy_excludes.py` +- `scripts/check_text_integrity.py` +- `scripts/generate_release_bundle.py` +- `scripts/generate_release_diff.py` +- `scripts/prepare_release_artifact.py` +- `scripts/publish_release_bundle.py` +- `scripts/run_btpanel_tests.py` +- `scripts/run_btpanel_tests_docker.cmd` +- `scripts/run_btpanel_tests_docker.sh` +- `scripts/run_local_release_gate.py` +- `scripts/scan_release_bundle.py` +- `scripts/scan_release_manifest.py` +- `scripts/verify_online_release.py` +- `scripts/write_release_metadata.py` +- `tests/test_btpanel_bootstrap_lock.py` +- `tests/test_publish_release_bundle.py` +- `tests/test_remote_archive_commands.py` +- `web/app-v2/assets/DashboardPage--9cH96CJ.js` +- `web/app-v2/assets/index-8f2_PJZM.js` +- `web/app-v2/assets/index-saKc1OPG.css` +- `web/app-v2/assets/query-B5fiFlYp.js` +- `web/app-v2/assets/react-DGbf-iXy.js` +- `web/app-v2/assets/rolldown-runtime-Bh1tDfsg.js` +- `web/app-v2/assets/table-C-Cg70AL.js` +- `web/app-v2/assets/ui-sq2aW9r5.js` +- `web/app-v2/index.html` +- `web/app/assets/AlertsPage-BZa6JbW1.js` +- `web/app/assets/AlertsPage-DkbuUWgZ.js` +- `web/app/assets/AuditPage-CrzMBMPz.js` +- `web/app/assets/AuditPage-DE5F-bot.js` +- `web/app/assets/BtPanelCrontabPage-CsxFXzwR.js` +- `web/app/assets/BtPanelCrontabPage-DFAuKeNI.js` +- `web/app/assets/BtPanelDatabasesPage-Cua9aENZ.js` +- `web/app/assets/BtPanelDatabasesPage-Cxt_i4Lp.js` +- `web/app/assets/BtPanelDomainsPage-9oKLFhi4.js` +- `web/app/assets/BtPanelDomainsPage-CSupPzQH.js` +- `web/app/assets/BtPanelLoginPage-DiKTnvyN.js` +- `web/app/assets/BtPanelLoginPage-Dp3X10Er.js` +- `web/app/assets/BtPanelMonitorPage-2tHIkE0-.js` +- `web/app/assets/BtPanelMonitorPage-Pm3fw_df.js` +- `web/app/assets/BtPanelMonitorPage-Td-5siPa.css` +- `web/app/assets/BtPanelServicesPage-B652HAqe.js` +- `web/app/assets/BtPanelServicesPage-DR8K2l-F.js` +- `web/app/assets/BtPanelSettingsPage-BBi6ttQm.js` +- `web/app/assets/BtPanelSettingsPage-DCQKy6JG.js` +- `web/app/assets/BtPanelSiteCreatePage-DLG_BM-C.js` +- `web/app/assets/BtPanelSiteCreatePage-DTCrnEa5.js` +- `web/app/assets/BtPanelSitesPage--EP6G5x3.js` +- `web/app/assets/BtPanelSitesPage-DWm4LMaV.js` +- `web/app/assets/BtPanelSslPage-BuiQZ1OJ.js` +- `web/app/assets/BtPanelSslPage-JWzPztxi.js` +- `web/app/assets/CommandsPage-BNtXfIOs.js` +- `web/app/assets/CommandsPage-CuHLCAHz.js` +- `web/app/assets/CredentialsPage-BPUBEwg9.js` +- `web/app/assets/CredentialsPage-Dc92himy.js` +- `web/app/assets/DashboardPage-DOc8f-2E.js` +- `web/app/assets/DashboardPage-DmG38yGr.css` +- `web/app/assets/DashboardPage-yElaw2BV.js` +- `web/app/assets/FilesPage-Be2kmggp.js` +- `web/app/assets/FilesPage-By07WOZR.css` +- `web/app/assets/FilesPage-DHwnC_As.js` +- `web/app/assets/FleetTrendEChart-DbHCuhg7.css` +- `web/app/assets/FleetTrendEChart-Dz57W1gD.js` +- `web/app/assets/FleetTrendEChart-lgdTP3R-.js` +- `web/app/assets/LoginPage-3NZZLudg.js` +- `web/app/assets/LoginPage-BKIQ3yge.js` +- `web/app/assets/LoginPage-BTEVFKpe.css` +- `web/app/assets/PushPage-BcIAXgGU.js` +- `web/app/assets/PushPage-DnVPUcAX.css` +- `web/app/assets/PushPage-_afVGSg2.js` +- `web/app/assets/RetriesPage-BdJWTxtp.js` +- `web/app/assets/RetriesPage-C70vf571.js` +- `web/app/assets/SchedulesPage-B47mlwc8.js` +- `web/app/assets/SchedulesPage-B9PFTR71.css` +- `web/app/assets/SchedulesPage-DM0j6jps.js` +- `web/app/assets/ScriptRunsPage-CW2C9c7C.css` +- `web/app/assets/ScriptRunsPage-DIeeXpt5.js` +- `web/app/assets/ScriptRunsPage-DeZ4sRVP.js` +- `web/app/assets/ScriptsPage-BWoaZhKf.js` +- `web/app/assets/ScriptsPage-BrIbZLc5.css` +- `web/app/assets/ScriptsPage-D8An6uCU.js` +- `web/app/assets/ServerCategoryPicker-BJm8eTKs.js` +- `web/app/assets/ServerCategoryPicker-BlyQtEug.js` +- `web/app/assets/ServerCategoryPicker-CHn3BA4z.css` +- `web/app/assets/ServerQuickAddDialogs-CoSabmGi.js` +- `web/app/assets/ServerQuickAddDialogs-zcLgLArY.js` +- `web/app/assets/ServerTransferPage-BdwpJEc4.css` +- `web/app/assets/ServerTransferPage-CWGjaCXR.js` +- `web/app/assets/ServerTransferPage-UYjJ6gzQ.js` +- `web/app/assets/ServersPage-CEOh0Y1q.css` +- `web/app/assets/ServersPage-D7wE_7hM.css` +- `web/app/assets/ServersPage-nR5-sjDp.js` +- `web/app/assets/ServersPage-nhvNw2_Z.js` +- `web/app/assets/SettingsPage-Am37wCvf.js` +- `web/app/assets/SettingsPage-FAHRRqSI.js` +- `web/app/assets/StatCardsRow-CWt_wcFU.js` +- `web/app/assets/StatCardsRow-CdfBB9tI.js` +- `web/app/assets/StatCardsRow-snFLCRnD.css` +- `web/app/assets/TerminalPage-CNkogNbG.js` +- `web/app/assets/TerminalPage-DX9qLtN2.js` +- `web/app/assets/TerminalPage-DvGyoWff.css` +- `web/app/assets/VAutocomplete-B1NSIBuv.js` +- `web/app/assets/VAutocomplete-CZMI3cz9.js` +- `web/app/assets/VAutocomplete-D48FgPMA.css` +- `web/app/assets/VCard-BCYbpv-w.css` +- `web/app/assets/VCard-BwwttBcf.js` +- `web/app/assets/VCard-pmpQR9O7.js` +- `web/app/assets/VCheckbox-BO-mWtTk.js` +- `web/app/assets/VCheckbox-ChRKi1kj.css` +- `web/app/assets/VCheckbox-RnLQ4WBr.js` +- `web/app/assets/VDataTable-ClfE0-sD.js` +- `web/app/assets/VDataTable-D61zAwkI.css` +- `web/app/assets/VDataTable-pRRodit6.js` +- `web/app/assets/VDataTableServer-BIMNqMvk.js` +- `web/app/assets/VDataTableServer-BysXbZ8p.js` +- `web/app/assets/VDialog-BFQGuuXw.js` +- `web/app/assets/VDialog-DP-kc9Gx.css` +- `web/app/assets/VDialog-nKJcjfNL.js` +- `web/app/assets/VExpansionPanels-ByOIBgKv.js` +- `web/app/assets/VExpansionPanels-CUfe7Zz5.css` +- `web/app/assets/VExpansionPanels-DUlWPWNJ.js` +- `web/app/assets/VForm-CAvEgM-c.js` +- `web/app/assets/VForm-uEqKaHHF.js` +- `web/app/assets/VRow-DIr3-SqU.css` +- `web/app/assets/VRow-qjxp2VJ5.js` +- `web/app/assets/VRow-rmSa_q-C.js` +- `web/app/assets/VSelect-3h-EI23Y.js` +- `web/app/assets/VSelect-BmvHN1-i.css` +- `web/app/assets/VSelect-BvR-yyLS.js` +- `web/app/assets/VSkeletonLoader-BW_kZu-c.js` +- `web/app/assets/VSkeletonLoader-CkAIsbz1.css` +- `web/app/assets/VSkeletonLoader-gL4kF19X.js` +- `web/app/assets/VSwitch-CIruBPax.js` +- `web/app/assets/VSwitch-DPhHiAlh.css` +- `web/app/assets/VSwitch-VDVIZTO7.js` +- `web/app/assets/VTable-BTftprIQ.js` +- `web/app/assets/VTable-Dq_7MYoa.css` +- `web/app/assets/VTable-w1sxrmvd.js` +- `web/app/assets/VTabs-BX9iYxVa.js` +- `web/app/assets/VTabs-Bq6bCiCC.js` +- `web/app/assets/VTabs-DjRHpLZ2.css` +- `web/app/assets/VTextField-B-BnrJAQ.js` +- `web/app/assets/VTextField-Bl91kSVs.css` +- `web/app/assets/VTextField-GUQr1tUN.js` +- `web/app/assets/VTextarea-BMTWx02W.js` +- `web/app/assets/VTextarea-DDl63AeD.css` +- `web/app/assets/VTextarea-PzNEhaMZ.js` +- `web/app/assets/VTooltip-BbRT1OE8.js` +- `web/app/assets/VTooltip-aiKJ1MVA.css` +- `web/app/assets/VTooltip-tVMR683k.js` +- `web/app/assets/WatchMetricsPage-BjqkPE11.css` +- `web/app/assets/WatchMetricsPage-CL8abV00.js` +- `web/app/assets/WatchMetricsPage-DByjAcyM.css` +- `web/app/assets/WatchMetricsPage-DY4qY84I.js` +- `web/app/assets/_plugin-vue_export-helper-BDNMzG2s.js` +- `web/app/assets/abap-08VXUWAP.js` +- `web/app/assets/apex-BWPQTe0t.js` +- `web/app/assets/apiError-BUh3zCmq.js` +- `web/app/assets/apiError-CpgZRt4T.js` +- `web/app/assets/auditNormalize-xrm2MK_4.js` +- `web/app/assets/azcli-Bc_sGQ0U.js` +- `web/app/assets/bat-i0X4ZdIN.js` +- `web/app/assets/bicep-B5-_aFwp.js` +- `web/app/assets/btpanel-BsL25ffd.js` +- `web/app/assets/btpanel-C_41A_fE.js` +- `web/app/assets/cameligo-DMUM7wLl.js` +- `web/app/assets/chunk-DK3Fl9T5.js` +- `web/app/assets/clojure-Cm7r79vr.js` +- `web/app/assets/codicon-ngg6Pgfi.ttf` +- `web/app/assets/coffee-Ba7i2nA0.js` +- `web/app/assets/cpp-C7h46wYY.js` +- `web/app/assets/csharp-BKxtCVv1.js` +- `web/app/assets/csp-bTuwJoIa.js` +- `web/app/assets/css-DIMkf-bt.js` +- `web/app/assets/css.worker-CvXBzhp8.js` +- `web/app/assets/cssMode-CnBxfcpP.js` +- `web/app/assets/cssMode-DoYviH0F.js` +- `web/app/assets/cypher-CVaqCwHa.js` +- `web/app/assets/dart-onAF5SnQ.js` +- `web/app/assets/dataTable-esDY4cOk.js` +- `web/app/assets/dist-DCycUQQT.js` +- `web/app/assets/dist-DMqKEX4A.js` +- `web/app/assets/dockerfile-DZFCIeNp.js` +- `web/app/assets/ecl-D05T4iGw.js` +- `web/app/assets/editor.worker-Cn2oRESe.js` +- `web/app/assets/elixir-6RTg0lbw.js` +- `web/app/assets/execDetailGrouping-B4lJ15VJ.js` +- `web/app/assets/execDetailGrouping-CuMwXaZZ.js` +- `web/app/assets/flow9-C5_-GSwl.js` +- `web/app/assets/freemarker2-BUYL0LB-.js` +- `web/app/assets/freemarker2-D0e9LerR.js` +- `web/app/assets/fsharp-C8Ef5oNN.js` +- `web/app/assets/go-C-y9NEjX.js` +- `web/app/assets/graphql-fmXr3nnJ.js` +- `web/app/assets/handlebars-C8vPzJIb.js` +- `web/app/assets/handlebars-ncsxj4cu.js` +- `web/app/assets/hcl-CpzslTdj.js` +- `web/app/assets/html-8-zTuU_5.js` +- `web/app/assets/html-DUT2IBNz.js` +- `web/app/assets/html.worker-BO6WuOEO.js` +- `web/app/assets/htmlMode-Bm_X9rOD.js` +- `web/app/assets/htmlMode-C8LJuzCB.js` +- `web/app/assets/index-BNJe_ogF.css` +- `web/app/assets/index-CDcgx9V8.js` +- `web/app/assets/index-a0fi5f-0.js` +- `web/app/assets/ini-sBoK_t0W.js` +- `web/app/assets/initMonaco-Cjv05FdX.js` +- `web/app/assets/initMonaco-DTDMLv7Z.js` +- `web/app/assets/initMonaco-DX2BVZF3.css` +- `web/app/assets/install-BY4V-NK4.js` +- `web/app/assets/install-bdNoH-9Q.js` +- `web/app/assets/java-BEtHBSE6.js` +- `web/app/assets/javascript-B4NqljlL.js` +- `web/app/assets/javascript-wfMVVC84.js` +- `web/app/assets/json.worker-BkJRGcCJ.js` +- `web/app/assets/jsonMode-B06-mTjs.js` +- `web/app/assets/jsonMode-B7uld_NH.js` +- `web/app/assets/julia-Bri6UV-V.js` +- `web/app/assets/kotlin-BOotOW0E.js` +- `web/app/assets/labels-C-A2d08W.js` +- `web/app/assets/less-B9JPFI3C.js` +- `web/app/assets/lexon-CfSJPG6W.js` +- `web/app/assets/liquid-B1dfVIHc.js` +- `web/app/assets/liquid-BxkVsnPx.js` +- `web/app/assets/lspLanguageFeatures-3WI5pdzZ.js` +- `web/app/assets/lspLanguageFeatures-CaMGOy4a.js` +- `web/app/assets/lua-CsQS60Ue.js` +- `web/app/assets/m3-D-oSqn_W.js` +- `web/app/assets/markdown-Cimd5fb3.js` +- `web/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf` +- `web/app/assets/materialdesignicons-webfont-CSr8KVlo.eot` +- `web/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2` +- `web/app/assets/materialdesignicons-webfont-PXm3-2wK.woff` +- `web/app/assets/mdx-B6lamGpH.js` +- `web/app/assets/mdx-lREt6kXe.js` +- `web/app/assets/mips-CIPQ_RoX.js` +- `web/app/assets/msdax-DauUninz.js` +- `web/app/assets/mysql-SOo6toE5.js` +- `web/app/assets/objective-c-FvmIjYaQ.js` +- `web/app/assets/paginatedFetch-CijkqaDG.js` +- `web/app/assets/paginatedFetch-VYTwXwno.js` +- `web/app/assets/pascal-DrH0SRf2.js` +- `web/app/assets/pascaligo-D-ptJ9y-.js` +- `web/app/assets/perl-oz_6vUea.js` +- `web/app/assets/pgsql-DTj74zXo.js` +- `web/app/assets/php-nr791fC2.js` +- `web/app/assets/pla-CopQ2nXW.js` +- `web/app/assets/postiats-43DmfD33.js` +- `web/app/assets/powerquery-D3hlyOfw.js` +- `web/app/assets/powershell-DmHpPYUd.js` +- `web/app/assets/protobuf-C531GsRP.js` +- `web/app/assets/pug-Z5eAx3Zn.js` +- `web/app/assets/python-Dt0lyPW-.js` +- `web/app/assets/python-DwgZPXhL.js` +- `web/app/assets/qsharp-DkqhCAOL.js` +- `web/app/assets/r-BwWrilGY.js` +- `web/app/assets/razor-B1t_g3Ke.js` +- `web/app/assets/razor-Dv9uiya3.js` +- `web/app/assets/redis-ClamHrr6.js` +- `web/app/assets/redshift-DT7zqm-g.js` +- `web/app/assets/remotePath-D3rG2Z3e.js` +- `web/app/assets/restructuredtext-BYgofb2h.js` +- `web/app/assets/roboto-latin-300-normal-BARJ-h6h.woff` +- `web/app/assets/roboto-latin-300-normal-CCzlftfr.woff2` +- `web/app/assets/roboto-latin-400-normal-BqEyEoaF.woff2` +- `web/app/assets/roboto-latin-400-normal-DyYNIH4P.woff` +- `web/app/assets/roboto-latin-500-normal-7RbcRiD8.woff2` +- `web/app/assets/roboto-latin-500-normal-DQZyH_nt.woff` +- `web/app/assets/roboto-latin-700-normal-BZpUvMxY.woff2` +- `web/app/assets/roboto-latin-700-normal-DLgJJpmK.woff` +- `web/app/assets/ruby-DezsRK8O.js` +- `web/app/assets/rust-DdL9SqIa.js` +- `web/app/assets/sb-CcwsVR0C.js` +- `web/app/assets/scala-DHpiXF5c.js` +- `web/app/assets/scheme-BeGwcela.js` +- `web/app/assets/scss-gp-XZpBa.js` +- `web/app/assets/serverSiteUrl-DDZRgSNA.js` +- `web/app/assets/shell-CC2rA5mh.js` +- `web/app/assets/solidity-BEEn4gHE.js` +- `web/app/assets/sophia-CRfGWb83.js` +- `web/app/assets/sparql-D_Lu-MrJ.js` +- `web/app/assets/sql-NEE52Syq.js` +- `web/app/assets/st-DbInun42.js` +- `web/app/assets/status-BrGQYcd-.js` +- `web/app/assets/status-XDgjPQrL.js` +- `web/app/assets/swift-Bxkupp3x.js` +- `web/app/assets/systemverilog-Bz4Y3fRF.js` +- `web/app/assets/tcl-DISqw1ZD.js` +- `web/app/assets/ts.worker-B6r0Skfj.js` +- `web/app/assets/tsMode-9bXRGx5g.js` +- `web/app/assets/tsMode-ChdzhlZm.js` +- `web/app/assets/twig-De2hgUGE.js` +- `web/app/assets/typescript-D3T7er6k.js` +- `web/app/assets/typescript-DRN2FRNg.js` +- `web/app/assets/typespec-B8J7ngcE.js` +- `web/app/assets/useBtPanelLogin-DCNB-U_J.js` +- `web/app/assets/useBtPanelLogin-DDcleXPj.js` +- `web/app/assets/useBtPanelPageLoad-CD81wbTP.js` +- `web/app/assets/useBtPanelPageLoad-CV_unS2-.js` +- `web/app/assets/usePageActivateRefresh-BntaOABK.js` +- `web/app/assets/usePageActivateRefresh-CqHSpagO.js` +- `web/app/assets/usePageAutoRefresh-DW8DyVQQ.js` +- `web/app/assets/usePageAutoRefresh-DvG6Fd0_.js` +- `web/app/assets/usePushStaging-BCIq1TPK.js` +- `web/app/assets/usePushStaging-CfCU7s8k.css` +- `web/app/assets/usePushStaging-DcgeHeSX.js` +- `web/app/assets/useServerCategories-BcvoZF0H.js` +- `web/app/assets/useServerCategories-V7luFEnW.js` +- `web/app/assets/useServerList-6fsbQxWS.js` +- `web/app/assets/useServerList-w3j3GRMG.js` +- `web/app/assets/useServerSearchHistory-BnDuA-7p.js` +- `web/app/assets/useServerSearchHistory-C3ha-zP2.css` +- `web/app/assets/useServerSearchHistory-CTnIHvTd.js` +- `web/app/assets/useSnackbar-u_XK4ZNm.js` +- `web/app/assets/useTerminalQuickCommands-CGHoPHjs.js` +- `web/app/assets/useTerminalQuickCommands-DKqOac6p.js` +- `web/app/assets/useTerminalQuickCommands-DyOcH0CY.css` +- `web/app/assets/useWatchPins-DTUw7j9v.js` +- `web/app/assets/useWatchPins-DxD91kwD.js` +- `web/app/assets/validation-BMzYTJL4.js` +- `web/app/assets/vb-DV3o63ZY.js` +- `web/app/assets/wgsl-DpFanUEy.js` +- `web/app/assets/xml-BFdUA9kH.js` +- `web/app/assets/xml-Btmx4yuJ.js` +- `web/app/assets/yaml-DKn-1xmA.js` +- `web/app/assets/yaml-tUm_O8N4.js` + +## Modified files + +- `.dockerignore` +- `Dockerfile` +- `Dockerfile.prod` +- `deploy/deploy-production.sh` +- `deploy/rsync-local-to-server.sh` +- `deploy/sync_webapp_to_container.sh` +- `docker-compose.yml` +- `docs/research/jumpserver-easynode-architecture-analysis.md` +- `requirements-dev.txt` +- `server/api/assets.py` +- `server/api/scripts.py` +- `server/api/sync_v2.py` +- `server/application/services/btpanel_service.py` +- `server/application/services/server_file_transfer_service.py` +- `server/infrastructure/btpanel/bootstrap_lock.py` +- `server/infrastructure/btpanel/login_url_lock.py` +- `server/infrastructure/btpanel/ssh_bootstrap.py` +- `server/infrastructure/ssh/remote_archive.py` +- `server/main.py` +- `server/utils/files_chmod_policy.py` +- `tests/test_btpanel_get_config.py` +- `tests/test_btpanel_login_url.py` +- `tests/test_btpanel_login_url_route.py` +- `tests/test_btpanel_ssh_bootstrap.py` +- `tests/test_file_permissions.py` +- `tests/test_server_file_transfer.py` + +## Deleted files + +- `docker/.env.example` +- `docker/.env.prod.example` +- `docker/profiles/1c4g.env` +- `docker/profiles/2c8g.env` +- `docker/profiles/4c16g.env` +- `docs/Nexus-功能说明书.html` +- `frontend/.env.development` +- `scripts/npm-proxy.env` +- `web/app/dist/assets/AlertsPage-ji5eXKTc.js` +- `web/app/dist/assets/AuditPage-C3fmqt6f.js` +- `web/app/dist/assets/CommandsPage-C6QrgHcR.js` +- `web/app/dist/assets/CredentialsPage-oLbFLgd2.js` +- `web/app/dist/assets/DashboardPage-CKfPpNOT.js` +- `web/app/dist/assets/FilesPage-BvG082ru.css` +- `web/app/dist/assets/FilesPage-DNKIBjNs.js` +- `web/app/dist/assets/LoginPage-D8ZqP1P_.js` +- `web/app/dist/assets/MonacoEditor-Sfe2OjtA.js` +- `web/app/dist/assets/PushPage-BDdmIVXZ.js` +- `web/app/dist/assets/PushPage-ewjA8E7l.css` +- `web/app/dist/assets/RetriesPage-BJbJspGS.js` +- `web/app/dist/assets/SchedulesPage-D6oD8ext.js` +- `web/app/dist/assets/ScriptsPage-CbZT-hk0.js` +- `web/app/dist/assets/ServersPage-B4elP3Z1.js` +- `web/app/dist/assets/ServersPage-DjRHpLZ2.css` +- `web/app/dist/assets/SettingsPage-BuPIwZWo.js` +- `web/app/dist/assets/TerminalPage-BGKczTgd.js` +- `web/app/dist/assets/TerminalPage-Ol9WjDlo.css` +- `web/app/dist/assets/VAlert-B33BooF0.css` +- `web/app/dist/assets/VAlert-DGGVwUFF.js` +- `web/app/dist/assets/VDataTable-0Xurhrxv.js` +- `web/app/dist/assets/VDataTable-DlpDfCoY.css` +- `web/app/dist/assets/VDataTableServer-FvIQviSN.js` +- `web/app/dist/assets/VDialog-BLibPcoM.js` +- `web/app/dist/assets/VDialog-DP-kc9Gx.css` +- `web/app/dist/assets/VFileInput-C6sFcwsM.js` +- `web/app/dist/assets/VFileInput-HYy7i9zh.css` +- `web/app/dist/assets/VRow-DIr3-SqU.css` +- `web/app/dist/assets/VRow-r8nkWmtD.js` +- `web/app/dist/assets/VSelect-ALpjlDrA.css` +- `web/app/dist/assets/VSelect-Bz5bHvpa.js` +- `web/app/dist/assets/VSelectionControl-BTVr3HQm.css` +- `web/app/dist/assets/VSelectionControl-C1Y9bNOF.js` +- `web/app/dist/assets/VSpacer-DWZdSXUK.js` +- `web/app/dist/assets/VSpacer-DfbUir7X.css` +- `web/app/dist/assets/VSwitch-DPhHiAlh.css` +- `web/app/dist/assets/VSwitch-DY1GlILd.js` +- `web/app/dist/assets/VTextarea-3TieuoAv.js` +- `web/app/dist/assets/VTextarea-DDl63AeD.css` +- `web/app/dist/assets/abap-08VXUWAP.js` +- `web/app/dist/assets/apex-BWPQTe0t.js` +- `web/app/dist/assets/azcli-Bc_sGQ0U.js` +- `web/app/dist/assets/bat-i0X4ZdIN.js` +- `web/app/dist/assets/bicep-B5-_aFwp.js` +- `web/app/dist/assets/cameligo-DMUM7wLl.js` +- `web/app/dist/assets/clojure-Cm7r79vr.js` +- `web/app/dist/assets/codicon-ngg6Pgfi.ttf` +- `web/app/dist/assets/coffee-Ba7i2nA0.js` +- `web/app/dist/assets/cpp-C7h46wYY.js` +- `web/app/dist/assets/csharp-BKxtCVv1.js` +- `web/app/dist/assets/csp-bTuwJoIa.js` +- `web/app/dist/assets/css-DIMkf-bt.js` +- `web/app/dist/assets/css.worker-CvXBzhp8.js` +- `web/app/dist/assets/cssMode-DMvENJ_w.js` +- `web/app/dist/assets/cypher-CVaqCwHa.js` +- `web/app/dist/assets/dart-onAF5SnQ.js` +- `web/app/dist/assets/dockerfile-DZFCIeNp.js` +- `web/app/dist/assets/ecl-D05T4iGw.js` +- `web/app/dist/assets/editor-DX2BVZF3.css` +- `web/app/dist/assets/editor.api2-MWf-eAi-.js` +- `web/app/dist/assets/elixir-6RTg0lbw.js` +- `web/app/dist/assets/flow9-C5_-GSwl.js` +- `web/app/dist/assets/freemarker2-DQGAilFk.js` +- `web/app/dist/assets/fsharp-C8Ef5oNN.js` +- `web/app/dist/assets/go-C-y9NEjX.js` +- `web/app/dist/assets/graphql-fmXr3nnJ.js` +- `web/app/dist/assets/handlebars-DCagTjhi.js` +- `web/app/dist/assets/hcl-CpzslTdj.js` +- `web/app/dist/assets/html-nrfydv4h.js` +- `web/app/dist/assets/html.worker-BO6WuOEO.js` +- `web/app/dist/assets/htmlMode-Bp4WNwyQ.js` +- `web/app/dist/assets/index-DOFNruam.js` +- `web/app/dist/assets/index-YClpuO23.css` +- `web/app/dist/assets/ini-sBoK_t0W.js` +- `web/app/dist/assets/java-BEtHBSE6.js` +- `web/app/dist/assets/javascript-B0RcszDa.js` +- `web/app/dist/assets/json.worker-BkJRGcCJ.js` +- `web/app/dist/assets/jsonMode-D0vdA3eU.js` +- `web/app/dist/assets/julia-Bri6UV-V.js` +- `web/app/dist/assets/kotlin-BOotOW0E.js` +- `web/app/dist/assets/less-B9JPFI3C.js` +- `web/app/dist/assets/lexon-CfSJPG6W.js` +- `web/app/dist/assets/liquid-DAtIwle-.js` +- `web/app/dist/assets/lspLanguageFeatures-DRDm87pn.js` +- `web/app/dist/assets/lua-CsQS60Ue.js` +- `web/app/dist/assets/m3-D-oSqn_W.js` +- `web/app/dist/assets/markdown-Cimd5fb3.js` +- `web/app/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf` +- `web/app/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot` +- `web/app/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2` +- `web/app/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff` +- `web/app/dist/assets/mdx-BvtNR28y.js` +- `web/app/dist/assets/mips-CIPQ_RoX.js` +- `web/app/dist/assets/monaco.contribution-fyH2rvpO.js` +- `web/app/dist/assets/msdax-DauUninz.js` +- `web/app/dist/assets/mysql-SOo6toE5.js` +- `web/app/dist/assets/objective-c-FvmIjYaQ.js` +- `web/app/dist/assets/pascal-DrH0SRf2.js` +- `web/app/dist/assets/pascaligo-D-ptJ9y-.js` +- `web/app/dist/assets/perl-oz_6vUea.js` +- `web/app/dist/assets/pgsql-DTj74zXo.js` +- `web/app/dist/assets/php-nr791fC2.js` +- `web/app/dist/assets/pla-CopQ2nXW.js` +- `web/app/dist/assets/postiats-43DmfD33.js` +- `web/app/dist/assets/powerquery-D3hlyOfw.js` +- `web/app/dist/assets/powershell-DmHpPYUd.js` +- `web/app/dist/assets/protobuf-C531GsRP.js` +- `web/app/dist/assets/pug-Z5eAx3Zn.js` +- `web/app/dist/assets/python-Y2lQRCrb.js` +- `web/app/dist/assets/qsharp-DkqhCAOL.js` +- `web/app/dist/assets/r-BwWrilGY.js` +- `web/app/dist/assets/razor-DHI-mrmN.js` +- `web/app/dist/assets/redis-ClamHrr6.js` +- `web/app/dist/assets/redshift-DT7zqm-g.js` +- `web/app/dist/assets/restructuredtext-BYgofb2h.js` +- `web/app/dist/assets/roboto-cyrillic-100-italic-CNP2SmvR.woff2` +- `web/app/dist/assets/roboto-cyrillic-100-italic-GfU4zM_J.woff` +- `web/app/dist/assets/roboto-cyrillic-100-normal-D_oR061d.woff2` +- `web/app/dist/assets/roboto-cyrillic-100-normal-w5umKD67.woff` +- `web/app/dist/assets/roboto-cyrillic-300-italic-Bbg0zipm.woff2` +- `web/app/dist/assets/roboto-cyrillic-300-italic-BfviqnaP.woff` +- `web/app/dist/assets/roboto-cyrillic-300-normal-DEFNdjk5.woff` +- `web/app/dist/assets/roboto-cyrillic-300-normal-DzUz0kzv.woff2` +- `web/app/dist/assets/roboto-cyrillic-400-italic-BSQCxleC.woff` +- `web/app/dist/assets/roboto-cyrillic-400-italic-CqC_ywG3.woff2` +- `web/app/dist/assets/roboto-cyrillic-400-normal-Bjg-1-sg.woff` +- `web/app/dist/assets/roboto-cyrillic-400-normal-CBPI_iaY.woff2` +- `web/app/dist/assets/roboto-cyrillic-500-italic-CFOZTHyM.woff2` +- `web/app/dist/assets/roboto-cyrillic-500-italic-UnqpRRY_.woff` +- `web/app/dist/assets/roboto-cyrillic-500-normal-CBKMylY4.woff` +- `web/app/dist/assets/roboto-cyrillic-500-normal-CLao9AfR.woff2` +- `web/app/dist/assets/roboto-cyrillic-700-italic-DhNJyfCT.woff2` +- `web/app/dist/assets/roboto-cyrillic-700-italic-K45OuYTL.woff` +- `web/app/dist/assets/roboto-cyrillic-700-normal-C2o7G-SM.woff2` +- `web/app/dist/assets/roboto-cyrillic-700-normal-DhZFXDSN.woff` +- `web/app/dist/assets/roboto-cyrillic-900-italic-CW7hKEUp.woff2` +- `web/app/dist/assets/roboto-cyrillic-900-italic-Dw8LBt_T.woff` +- `web/app/dist/assets/roboto-cyrillic-900-normal-B-XH5ueX.woff` +- `web/app/dist/assets/roboto-cyrillic-900-normal-BVOxCBIE.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-100-italic-CHVV4rI0.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-100-italic-Drj7gMwC.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-100-normal-mbO7vZh1.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-100-normal-uxSc4Dbo.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-300-italic-D2iEI_X7.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-300-italic-OnrzXwVD.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-300-normal-D7ank4TF.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-300-normal-DIxttMbC.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-400-italic-4ESj9BbU.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-400-italic-MZ-G6OiW.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-400-normal-CaK1767H.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-400-normal-qHufge6k.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-500-italic-C-yqETei.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-500-italic-ExZ7-vA1.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-500-normal-BWC_xYeb.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-500-normal-DqF2hftb.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-700-italic-BPGsVrTY.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-700-italic-DEF-7i1Y.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-700-normal-CI7FH63F.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-700-normal-DmFxo5wj.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-900-italic-CSUk7NP9.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-900-italic-acKlpJv6.woff2` +- `web/app/dist/assets/roboto-cyrillic-ext-900-normal-Dy18Zgm9.woff` +- `web/app/dist/assets/roboto-cyrillic-ext-900-normal-IZ2B0aiV.woff2` +- `web/app/dist/assets/roboto-greek-100-italic-BByoHCxw.woff2` +- `web/app/dist/assets/roboto-greek-100-italic-Cy3jdfDA.woff` +- `web/app/dist/assets/roboto-greek-100-normal-DgpMWfbq.woff2` +- `web/app/dist/assets/roboto-greek-100-normal-ZuTz319d.woff` +- `web/app/dist/assets/roboto-greek-300-italic-BlmQgaZh.woff` +- `web/app/dist/assets/roboto-greek-300-italic-CznjWsFz.woff2` +- `web/app/dist/assets/roboto-greek-300-normal-C_Dgaih9.woff` +- `web/app/dist/assets/roboto-greek-300-normal-DJEM9B4Z.woff2` +- `web/app/dist/assets/roboto-greek-400-italic-BUWCAFe1.woff` +- `web/app/dist/assets/roboto-greek-400-italic-DnTUMkdF.woff2` +- `web/app/dist/assets/roboto-greek-400-normal-Bb5mj_fZ.woff` +- `web/app/dist/assets/roboto-greek-400-normal-ai2Z1K3C.woff2` +- `web/app/dist/assets/roboto-greek-500-italic-Dqz20l7v.woff2` +- `web/app/dist/assets/roboto-greek-500-italic-xdwGU0k_.woff` +- `web/app/dist/assets/roboto-greek-500-normal-C9AnhcmC.woff2` +- `web/app/dist/assets/roboto-greek-500-normal-oCqhoyfc.woff` +- `web/app/dist/assets/roboto-greek-700-italic-DawR0hnC.woff2` +- `web/app/dist/assets/roboto-greek-700-italic-P6YHZuSy.woff` +- `web/app/dist/assets/roboto-greek-700-normal-0aHWxGLu.woff2` +- `web/app/dist/assets/roboto-greek-700-normal-DjRqqLBV.woff` +- `web/app/dist/assets/roboto-greek-900-italic-B7xCHAj9.woff` +- `web/app/dist/assets/roboto-greek-900-italic-DvS3zuic.woff2` +- `web/app/dist/assets/roboto-greek-900-normal-B5AAzeOC.woff2` +- `web/app/dist/assets/roboto-greek-900-normal-DWdVoZCP.woff` +- `web/app/dist/assets/roboto-latin-100-italic-CMH2tK0H.woff2` +- `web/app/dist/assets/roboto-latin-100-italic-DZWoXrp_.woff` +- `web/app/dist/assets/roboto-latin-100-normal-CGMktwvD.woff` +- `web/app/dist/assets/roboto-latin-100-normal-vTzS_GaG.woff2` +- `web/app/dist/assets/roboto-latin-300-italic-B5i8yzYq.woff` +- `web/app/dist/assets/roboto-latin-300-italic-zCpkrxSM.woff2` +- `web/app/dist/assets/roboto-latin-300-normal-BARJ-h6h.woff` +- `web/app/dist/assets/roboto-latin-300-normal-CCzlftfr.woff2` +- `web/app/dist/assets/roboto-latin-400-italic-C2a9rKC1.woff` +- `web/app/dist/assets/roboto-latin-400-italic-CSuqwwKr.woff2` +- `web/app/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2` +- `web/app/dist/assets/roboto-latin-400-normal-DyYNIH4P.woff` +- `web/app/dist/assets/roboto-latin-500-italic-Bzx6Emny.woff` +- `web/app/dist/assets/roboto-latin-500-italic-DOe3GFcv.woff2` +- `web/app/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2` +- `web/app/dist/assets/roboto-latin-500-normal-DQZyH_nt.woff` +- `web/app/dist/assets/roboto-latin-700-italic-CC4lh_E9.woff` +- `web/app/dist/assets/roboto-latin-700-italic-Cb9agdGy.woff2` +- `web/app/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2` +- `web/app/dist/assets/roboto-latin-700-normal-DLgJJpmK.woff` +- `web/app/dist/assets/roboto-latin-900-italic-BENCS7Df.woff2` +- `web/app/dist/assets/roboto-latin-900-italic-DNjw4RGV.woff` +- `web/app/dist/assets/roboto-latin-900-normal-F72S18P8.woff` +- `web/app/dist/assets/roboto-latin-900-normal-lk0O8k6m.woff2` +- `web/app/dist/assets/roboto-latin-ext-100-italic-BzoOrAFP.woff` +- `web/app/dist/assets/roboto-latin-ext-100-italic-CprbSwJf.woff2` +- `web/app/dist/assets/roboto-latin-ext-100-normal-Bue4UH9m.woff2` +- `web/app/dist/assets/roboto-latin-ext-100-normal-CRE1JcN2.woff` +- `web/app/dist/assets/roboto-latin-ext-300-italic-BaJRnD9g.woff` +- `web/app/dist/assets/roboto-latin-ext-300-italic-Bavh33sq.woff2` +- `web/app/dist/assets/roboto-latin-ext-300-normal-B90pq-BC.woff2` +- `web/app/dist/assets/roboto-latin-ext-300-normal-CTCCHkZF.woff` +- `web/app/dist/assets/roboto-latin-ext-400-italic-BilqUbDw.woff2` +- `web/app/dist/assets/roboto-latin-ext-400-italic-g8iNtijM.woff` +- `web/app/dist/assets/roboto-latin-ext-400-normal-C3tdtHj3.woff2` +- `web/app/dist/assets/roboto-latin-ext-400-normal-scX0fKtV.woff` +- `web/app/dist/assets/roboto-latin-ext-500-italic-B5teBpxj.woff2` +- `web/app/dist/assets/roboto-latin-ext-500-italic-BlZHPPdA.woff` +- `web/app/dist/assets/roboto-latin-ext-500-normal-Cyc0AKLz.woff` +- `web/app/dist/assets/roboto-latin-ext-500-normal-pMCM9Ixg.woff2` +- `web/app/dist/assets/roboto-latin-ext-700-italic-C8tIie4u.woff` +- `web/app/dist/assets/roboto-latin-ext-700-italic-CBdFfgzf.woff2` +- `web/app/dist/assets/roboto-latin-ext-700-normal-BUhwtWwy.woff` +- `web/app/dist/assets/roboto-latin-ext-700-normal-DSBUz0N1.woff2` +- `web/app/dist/assets/roboto-latin-ext-900-italic-CFogYjYt.woff` +- `web/app/dist/assets/roboto-latin-ext-900-italic-CgK4zYaI.woff2` +- `web/app/dist/assets/roboto-latin-ext-900-normal-Bg1HnWxG.woff2` +- `web/app/dist/assets/roboto-latin-ext-900-normal-CUx1IrMY.woff` +- `web/app/dist/assets/roboto-math-100-italic-B4vNMY8j.woff` +- `web/app/dist/assets/roboto-math-100-italic-C4gy5Bp3.woff2` +- `web/app/dist/assets/roboto-math-100-normal-3u4f34A3.woff2` +- `web/app/dist/assets/roboto-math-100-normal-DzbsQ8e_.woff` +- `web/app/dist/assets/roboto-math-300-italic-CIRUIJB7.woff` +- `web/app/dist/assets/roboto-math-300-italic-XxAa5nOs.woff2` +- `web/app/dist/assets/roboto-math-300-normal-5dF_7mZP.woff2` +- `web/app/dist/assets/roboto-math-300-normal-Ds0YpBw2.woff` +- `web/app/dist/assets/roboto-math-400-italic-BpybG2ZH.woff2` +- `web/app/dist/assets/roboto-math-400-italic-CXOsqNh0.woff` +- `web/app/dist/assets/roboto-math-400-normal-BEFej5gc.woff2` +- `web/app/dist/assets/roboto-math-400-normal-C9RxBKAh.woff` +- `web/app/dist/assets/roboto-math-500-italic-BaDHGWdF.woff2` +- `web/app/dist/assets/roboto-math-500-italic-Dr-Gj3Mh.woff` +- `web/app/dist/assets/roboto-math-500-normal-C-7mKPO3.woff` +- `web/app/dist/assets/roboto-math-500-normal-C4NU9gLX.woff2` +- `web/app/dist/assets/roboto-math-700-italic-B5uBDPR4.woff` +- `web/app/dist/assets/roboto-math-700-italic-BTlAqkAP.woff2` +- `web/app/dist/assets/roboto-math-700-normal-B8YqGHVc.woff2` +- `web/app/dist/assets/roboto-math-700-normal-DVoD5t2k.woff` +- `web/app/dist/assets/roboto-math-900-italic-B-PPazk1.woff` +- `web/app/dist/assets/roboto-math-900-italic-D-7BUet6.woff2` +- `web/app/dist/assets/roboto-math-900-normal-DQ66ivDi.woff2` +- `web/app/dist/assets/roboto-math-900-normal-Dmeiz_CW.woff` +- `web/app/dist/assets/roboto-symbols-100-italic-DSe_C1Iy.woff` +- `web/app/dist/assets/roboto-symbols-100-italic-DkwGdnY5.woff2` +- `web/app/dist/assets/roboto-symbols-100-normal-CjKjWFkd.woff2` +- `web/app/dist/assets/roboto-symbols-100-normal-CqmTeVyX.woff` +- `web/app/dist/assets/roboto-symbols-300-italic-BfpS6Q35.woff2` +- `web/app/dist/assets/roboto-symbols-300-italic-Dm46KJRd.woff` +- `web/app/dist/assets/roboto-symbols-300-normal-BCnjhQd_.woff` +- `web/app/dist/assets/roboto-symbols-300-normal-DDU7avhj.woff2` +- `web/app/dist/assets/roboto-symbols-400-italic-CQIwDYNA.woff` +- `web/app/dist/assets/roboto-symbols-400-italic-DJOHuWBY.woff2` +- `web/app/dist/assets/roboto-symbols-400-normal-CB1Ce4Gk.woff2` +- `web/app/dist/assets/roboto-symbols-400-normal-DLYbZahX.woff` +- `web/app/dist/assets/roboto-symbols-500-italic-BiGHMB5Q.woff2` +- `web/app/dist/assets/roboto-symbols-500-italic-Dz3aAlfh.woff` +- `web/app/dist/assets/roboto-symbols-500-normal-B_CZKVJS.woff2` +- `web/app/dist/assets/roboto-symbols-500-normal-F7c8nfcH.woff` +- `web/app/dist/assets/roboto-symbols-700-italic-Bj79QYOr.woff2` +- `web/app/dist/assets/roboto-symbols-700-italic-Dm-8FOP3.woff` +- `web/app/dist/assets/roboto-symbols-700-normal-BiFDindJ.woff2` +- `web/app/dist/assets/roboto-symbols-700-normal-BoS6HWkc.woff` +- `web/app/dist/assets/roboto-symbols-900-italic-BZDs0BY5.woff` +- `web/app/dist/assets/roboto-symbols-900-italic-QOKtSfbw.woff2` +- `web/app/dist/assets/roboto-symbols-900-normal-1vlkxR2C.woff2` +- `web/app/dist/assets/roboto-symbols-900-normal-9XmQV1ku.woff` +- `web/app/dist/assets/roboto-vietnamese-100-italic-BnSYtt9K.woff2` +- `web/app/dist/assets/roboto-vietnamese-100-italic-Hu6kAu-e.woff` +- `web/app/dist/assets/roboto-vietnamese-100-normal-Cc5a3-TP.woff` +- `web/app/dist/assets/roboto-vietnamese-100-normal-KgOkQYnu.woff2` +- `web/app/dist/assets/roboto-vietnamese-300-italic-B9qvi8_k.woff` +- `web/app/dist/assets/roboto-vietnamese-300-italic-CtUKlc4H.woff2` +- `web/app/dist/assets/roboto-vietnamese-300-normal-BPvXm_f1.woff2` +- `web/app/dist/assets/roboto-vietnamese-300-normal-INUupD3o.woff` +- `web/app/dist/assets/roboto-vietnamese-400-italic-C41J4i52.woff2` +- `web/app/dist/assets/roboto-vietnamese-400-italic-RF0eew8q.woff` +- `web/app/dist/assets/roboto-vietnamese-400-normal-D2PTxGxD.woff2` +- `web/app/dist/assets/roboto-vietnamese-400-normal-DnpnVwnf.woff` +- `web/app/dist/assets/roboto-vietnamese-500-italic-BddoBFd0.woff2` +- `web/app/dist/assets/roboto-vietnamese-500-italic-CmEfQ1B1.woff` +- `web/app/dist/assets/roboto-vietnamese-500-normal-B3ncpOoB.woff2` +- `web/app/dist/assets/roboto-vietnamese-500-normal-D380IkQ8.woff` +- `web/app/dist/assets/roboto-vietnamese-700-italic-By4b4cXv.woff` +- `web/app/dist/assets/roboto-vietnamese-700-italic-CVc74C74.woff2` +- `web/app/dist/assets/roboto-vietnamese-700-normal-BEVeWqJt.woff2` +- `web/app/dist/assets/roboto-vietnamese-700-normal-DsFyXAL4.woff` +- `web/app/dist/assets/roboto-vietnamese-900-italic-D0nMCc5h.woff` +- `web/app/dist/assets/roboto-vietnamese-900-italic-hVMOC7we.woff2` +- `web/app/dist/assets/roboto-vietnamese-900-normal-C48YQOjq.woff` +- `web/app/dist/assets/roboto-vietnamese-900-normal-D-H2ldSl.woff2` +- `web/app/dist/assets/ruby-DezsRK8O.js` +- `web/app/dist/assets/rust-DdL9SqIa.js` +- `web/app/dist/assets/sb-CcwsVR0C.js` +- `web/app/dist/assets/scala-DHpiXF5c.js` +- `web/app/dist/assets/scheme-BeGwcela.js` +- `web/app/dist/assets/scss-gp-XZpBa.js` +- `web/app/dist/assets/shell-CC2rA5mh.js` +- `web/app/dist/assets/solidity-BEEn4gHE.js` +- `web/app/dist/assets/sophia-CRfGWb83.js` +- `web/app/dist/assets/sparql-D_Lu-MrJ.js` +- `web/app/dist/assets/sql-NEE52Syq.js` +- `web/app/dist/assets/st-DbInun42.js` +- `web/app/dist/assets/status-CNSle5LJ.js` +- `web/app/dist/assets/swift-Bxkupp3x.js` +- `web/app/dist/assets/systemverilog-Bz4Y3fRF.js` +- `web/app/dist/assets/tcl-DISqw1ZD.js` +- `web/app/dist/assets/ts.worker-B6r0Skfj.js` +- `web/app/dist/assets/tsMode-B1_NfjHa.js` +- `web/app/dist/assets/twig-De2hgUGE.js` +- `web/app/dist/assets/typescript-B9Bb_0iF.js` +- `web/app/dist/assets/typespec-B8J7ngcE.js` +- `web/app/dist/assets/useServerList-CokRfazm.js` +- `web/app/dist/assets/useSnackbar-D7YOfKok.js` +- `web/app/dist/assets/validation-Yxuom3Du.js` +- `web/app/dist/assets/vb-DV3o63ZY.js` +- `web/app/dist/assets/wgsl-DpFanUEy.js` +- `web/app/dist/assets/workers-DLQCZ--h.js` +- `web/app/dist/assets/xml-BsJ5i10T.js` +- `web/app/dist/assets/yaml-DT_Rfd72.js` +- `web/app/dist/favicon.ico` +- `web/app/dist/index.html` diff --git a/docs/release/20260708/Nexus-release-index-20260708.md b/docs/release/20260708/Nexus-release-index-20260708.md new file mode 100644 index 00000000..dd75b398 --- /dev/null +++ b/docs/release/20260708/Nexus-release-index-20260708.md @@ -0,0 +1,148 @@ +# Nexus Release Index — 2026-07-08 + +## Status + +```text +local_artifact_ready=true +online_deployed=false +remote_ssh_blocked_by_codex_approval=true +``` + +Codex has not modified the production server in this phase. Remote SSH/network checks were blocked by the approval auto-review endpoint returning HTTP 404. + +## Artifact + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +SHA256: + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +Standard checksum file: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz.sha256 +``` + +Metadata: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.metadata.json +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-artifact-metadata-20260708.md +``` + +Current artifact facts: + +```text +size_mb=27.02 +entries=2409 +files=2409 +``` + +## Main documents + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-ready-checklist-20260708.md +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-manual-deploy-commands-20260708.md +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-app-v2-双后台实施说明-20260707.md +``` + +## One-command local preparation + +Run from the source snapshot: + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\prepare_release_artifact.py --old-snapshot C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --output-dir C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs --name nexus-release-20260708 +``` + +Verified result: + +```text +[prepare-release-artifact] OK +added=381 modified=26 deleted=347 +60 passed in 1.70s +OK: release bundle looks clean +Validate-only mode: tarball checksum and content validation passed +``` + +## Server-side apply command + +Upload: + +```cmd +scp C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz nexus:/tmp/nexus-release-20260708.tar.gz +``` + +If the new apply script is not yet on the server, extract it first: + +```bash +cd /tmp +tar xzf nexus-release-20260708.tar.gz nexus-release/deploy/apply-release-bundle.sh +``` + +Validate only: + +```bash +bash /tmp/nexus-release/deploy/apply-release-bundle.sh --validate-only \ + --tarball /tmp/nexus-release-20260708.tar.gz \ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +Apply: + +```bash +sudo bash /tmp/nexus-release/deploy/apply-release-bundle.sh \ + --tarball /tmp/nexus-release-20260708.tar.gz \ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \ + --deploy-path /opt/nexus +``` + +## Online verification after deploy + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip +``` + +Expected: + +```text +OK: health +OK: /app +OK: /app-v2 +OK: online release verification passed +``` + +## BtPanel verification + +After deploy: + +1. Existing `/app/` works. +2. New `/app-v2/` works. +3. BtPanel one-click login works. +4. Same server duplicate one-click login is serialized; conflict returns HTTP 409. +5. Newly added BtPanel servers get hardcoded_3600 detection/repair before one-click login. +6. Multiple BtPanel sessions remain logged in after 2 hours and refresh. + + + +## Server preflight command + +Before applying the release on the production host, run: + +```bash +bash /tmp/nexus-release/deploy/preflight-release-host.sh \ + --tarball /tmp/nexus-release-20260708.tar.gz \ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \ + --deploy-path /opt/nexus +``` + +This checks required commands, checksum, release tar paths, deploy path, runtime hints, disk space, and current local endpoints before applying changes. + +- `Nexus-release-text-integrity-gate-20260708.md`:本轮新增文本完整性 Gate、文档控制字符修复和最新制品 SHA 记录。 +- `Nexus-release-python-publisher-20260708.md`:Python 版发布辅助脚本说明,避免 PowerShell 管道/引号/编码问题。 +- `Nexus-release-publisher-tests-20260708.md`:发布辅助脚本离线测试覆盖与 diff manifest 排除 tmp 修复记录。 diff --git a/docs/release/20260708/Nexus-release-publisher-tests-20260708.md b/docs/release/20260708/Nexus-release-publisher-tests-20260708.md new file mode 100644 index 00000000..70efe3fd --- /dev/null +++ b/docs/release/20260708/Nexus-release-publisher-tests-20260708.md @@ -0,0 +1,55 @@ +# Nexus 发布辅助脚本测试覆盖记录(2026-07-08) + +## 本轮目的 + +继续降低线上发布风险:为 `scripts/publish_release_bundle.py` 增加离线单元测试,并修复 diff manifest 对测试临时文件的误收录。 + +## 新增测试 + +文件: + +```text +work\nexus_code_context\snapshots\nexus-source-20260707-112042\tests\test_publish_release_bundle.py +``` + +覆盖 5 个场景: + +1. 默认模式:上传 + preflight,不执行 apply; +2. 显式 `--apply`:preflight 后才执行 `apply-release-bundle.sh`; +3. `--no-upload --dry-run`:只生成远端 SSH 命令,不触发 scp; +4. SHA256 mismatch:在任何远端命令前失败; +5. 远端路径:始终使用 POSIX `/tmp/...`,避免 Windows `\tmp\...`。 + +## Gate 更新 + +`run_local_release_gate.py --with-pytest` 现在会执行: + +- 宝塔专项测试:60 个; +- 发布辅助脚本测试:5 个。 + +合计:65 个 Python 测试。 + +## Manifest 修复 + +`scripts/generate_release_diff.py` 新增排除 `tmp` 目录,避免测试临时 tarball 进入 diff manifest。 + +已确认: + +- release bundle 不包含 `tmp/pytest-publish-release`; +- diff manifest 不包含 `tmp/pytest-publish-release`; +- package scan 通过; +- validate-only 通过。 + +## 最新发布包 + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +SHA256: + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +entries:`2409` diff --git a/docs/release/20260708/Nexus-release-python-publisher-20260708.md b/docs/release/20260708/Nexus-release-python-publisher-20260708.md new file mode 100644 index 00000000..4758d56f --- /dev/null +++ b/docs/release/20260708/Nexus-release-python-publisher-20260708.md @@ -0,0 +1,81 @@ +# Nexus Python 发布辅助脚本(2026-07-08) + +## 目的 + +为避免 Windows PowerShell 管道、编码和引号问题,新增 Python 发布辅助脚本: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\publish_release_bundle.py +``` + +它使用 `subprocess` 参数列表调用 `scp` / `ssh`,不依赖本地 shell 管道;默认只上传发布包并执行服务器侧 preflight,不会直接发布。 + +## 最新发布包 + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +SHA256: + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +## 推荐使用方式 + +### 1. 只上传并执行 preflight(默认安全模式) + +```cmd +cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 +.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^ + --remote nexus ^ + --tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +### 2. 如果不使用 SSH config + +```cmd +cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 +.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^ + --remote root@你的线上服务器IP ^ + --tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^ + --deploy-path /opt/nexus +``` + +### 3. preflight 确认无误后执行 apply + +```cmd +cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 +.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^ + --remote nexus ^ + --tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^ + --apply +``` + +## 行为说明 + +脚本会: + +1. 本地校验 tarball SHA256; +2. 使用 `scp` 上传到 `/tmp/nexus-release-20260708.tar.gz`; +3. 在远端从发布包解出: + - `deploy/preflight-release-host.sh` + - `deploy/apply-release-bundle.sh` +4. 执行 preflight; +5. 只有传入 `--apply` 时才执行正式 apply。 + +## 已验证 + +- `--help` 正常; +- `--dry-run --no-upload` 正常; +- Windows 下远端路径保持 POSIX `/tmp/...`,不会被转换成 `\tmp\...`; +- 已纳入 `scripts/run_local_release_gate.py` 的 `py_compile`。 + +## 本轮新增测试 + +- `tests/test_publish_release_bundle.py`:5 个离线测试,覆盖默认 preflight、显式 apply、SHA mismatch、no-upload dry-run、远端 POSIX 路径。 +- 已纳入 `scripts/run_local_release_gate.py --with-pytest`。 diff --git a/docs/release/20260708/Nexus-release-ready-checklist-20260708.md b/docs/release/20260708/Nexus-release-ready-checklist-20260708.md new file mode 100644 index 00000000..359b3eb3 --- /dev/null +++ b/docs/release/20260708/Nexus-release-ready-checklist-20260708.md @@ -0,0 +1,495 @@ +# Nexus 发布前清单(2026-07-08) + +## 结论 + +当前本地快照已经通过完整发布前 Gate: + +```text +py_compile + /app-v2 build:safe + app-v2 sensitive scan + 宝塔 pytest +``` + +最终结果: + +```text +[release-gate] OK +60 passed in 5.33s +``` + +## 当前源码快照 + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 +``` + +对比基准快照: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 +``` + +完整差异清单: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md +``` + +## 差异清单汇总 + +重新生成后的清单已经排除: + +- `.git` +- `node_modules` +- `.venv-*` +- `venv` +- `__pycache__` +- `.ruff_cache` +- `.playwright-mcp` +- `.cursor` +- `.pytest_cache` +- `*.pyc` +- `*.pyo` +- `*.bak-` +- `2025.2` +- `=2025.2` +- `tmp/nexus-frontend.tar.gz` + +当前差异统计: + +```text +added=381 +modified=26 +deleted=347 +``` + +注意:差异里包含大量旧 `/app/` 静态构建产物变动。如果只发布本轮宝塔稳定性与 `/app-v2`,可以优先发布“核心发布集合”,不要盲目同步整个快照目录。 + +## 核心发布集合 + +### 后端宝塔一键登录稳定性 + +```text +server/application/services/btpanel_service.py +server/infrastructure/btpanel/bootstrap_lock.py +server/infrastructure/btpanel/login_url_lock.py +server/infrastructure/btpanel/ssh_bootstrap.py +server/main.py +``` + +### API / 服务相关历史改动 + +差异清单中还有以下后端文件发生变化,发布前需要按实际范围确认是否一起发布: + +```text +server/api/assets.py +server/api/scripts.py +server/api/sync_v2.py +server/application/services/server_file_transfer_service.py +server/infrastructure/ssh/remote_archive.py +server/utils/files_chmod_policy.py +``` + +这些文件属于此前安全巡检/文件传输/脚本执行相关改动,不是本轮宝塔一键登录最小集合;如果线上已经发布过,可跳过或只按 diff 覆盖。 + +### `/app-v2` 双后台 + +源码: + +```text +frontend-v2/ +``` + +构建产物: + +```text +web/app-v2/ +``` + +后端静态挂载: + +```text +server/main.py +``` + +Docker: + +```text +Dockerfile +Dockerfile.prod +``` + +### 测试与发布脚本 + +```text +requirements-dev.txt +scripts/check_backend_test_env.py +scripts/generate_release_diff.py +scripts/run_btpanel_tests.py +scripts/run_btpanel_tests_docker.cmd +scripts/run_btpanel_tests_docker.sh +scripts/run_local_release_gate.py +scripts/scan_release_manifest.py +``` + +### 测试文件 + +```text +tests/test_btpanel_bootstrap_lock.py +tests/test_btpanel_get_config.py +tests/test_btpanel_login_url.py +tests/test_btpanel_login_url_route.py +tests/test_btpanel_ssh_bootstrap.py +``` + +## 发布前验证命令 + +### 1. 重新生成差异清单 + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\generate_release_diff.py --old C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --new C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --output C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md +``` + +结果: + +```text +added=369 modified=21 deleted=347 +``` + +### 2. 扫描发布候选文件是否包含已知环境泄露标记 + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\scan_release_manifest.py --root C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --manifest C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md +``` + +结果: + +```text +OK: no known environment leak markers found in release manifest entries +``` + +### 3. 完整发布前 Gate + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\run_local_release_gate.py --with-pytest +``` + +结果: + +```text +[app-v2-sensitive-scan] OK: scanned 9 file(s), no high-confidence secrets found +collected 60 items +60 passed in 1.79s +[release-gate] OK +``` + +最新 Gate 追加确认: + +```text +bash -n deploy/sync_webapp_to_container.sh +60 passed in 1.70s +[release-gate] OK +``` + +### 4. 生成干净发布包 + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\generate_release_bundle.py --root C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --output C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +结果: + +```text +files=2409 +``` + +发布包位置: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +发布包元数据: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.metadata.json +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-artifact-metadata-20260708.md +``` + +SHA256: + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +### 5. 扫描发布包 + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\scan_release_bundle.py C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +结果: + +```text +size_mb=27.02 +entries=2409 +OK: release bundle looks clean +``` + +### 6. 部署排除规则自检 + +`scripts/run_local_release_gate.py` 现在会自动执行: + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\check_deploy_excludes.py +``` + +结果: + +```text +OK: deploy exclusion patterns are present +``` + +### 7. 发布后线上验证脚本 + +新增: + +```text +scripts/verify_online_release.py +``` + +用途: + +- 检查 `/health` +- 检查旧后台 `/app/` +- 检查新后台 `/app-v2/` +- 检查页面里不要出现 `全局 API Key`、`Global API Key`、`tmp_login`、`tmp_token` + +发布后执行: + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip +``` + +## 明确不发布 + +以下内容不要同步到正式服务器: + +```text +.venv-py312-codex/ +.venv-py314/ +venv/ +frontend-v2/node_modules/ +.pytest_cache/ +__pycache__/ +.ruff_cache/ +.cursor/ +.playwright-mcp/ +frontend-v2/node_modules/ +*.pyc +*.pyo +*.bak- +2025.2 +=2025.2 +tmp/nexus-frontend.tar.gz +``` + +其中 `2025.2` / `=2025.2` 是 Windows CMD 执行 `tzdata>=2025.2` 时可能产生的重定向误文件,绝对不要发布。 + +## 建议发布顺序 + +### 方案 A:Docker 镜像重建发布(推荐) + +1. 使用当前快照作为构建上下文,但确保 `.dockerignore` 排除虚拟环境、node_modules、缓存和临时文件。 +2. 构建生产镜像。 +3. 启动新容器前备份旧容器镜像/tag 和数据库。 +4. 发布后执行线上验证。 + +优点:静态资源、后端代码、依赖环境一致性更好。 + +当前已修复: + +- `.dockerignore` 已排除 `frontend-v2/node_modules`、`.venv-*`、`.pytest_cache`、`2025.2`、`*.bak-` 等。 +- `deploy/rsync-local-to-server.sh` 已排除 `frontend-v2/node_modules`、`.venv-*`、`.pytest_cache`、`2025.2`、`*.bak-` 等。 +- `scripts/run_local_release_gate.py` 已增加 `bash -n` 检查 `deploy/deploy-production.sh` 与 `deploy/rsync-local-to-server.sh`。 +- `deploy/deploy-production.sh` 发布后健康检查已增加 `/app-v2/`。 +- `deploy/deploy-production.sh` 已显式构建并上传 `web/app-v2`,不只依赖 Dockerfile 构建。 +- `deploy/apply-release-bundle.sh` 已新增,可在服务器侧对 tar.gz 发布包执行 SHA256 校验、备份、解包、重建和本机验证。 +- `deploy/sync_webapp_to_container.sh` 已同步 `web/app-v2` 到容器,并校验 `/app-v2/` 主 bundle。 +- `scripts/run_local_release_gate.py` 已增加 `bash -n deploy/sync_webapp_to_container.sh`。 +- `scripts/check_deploy_excludes.py` 会防止发布排除规则未来被误删。 + +### 当前远程执行状态 + +本轮尝试对线上 `Host nexus` 做只读 SSH 探测时,Codex 审批系统返回 404 并拒绝执行。不是 SSH 凭据或代码错误,而是自动审批代理不可用。 + +因此本轮没有实际修改线上服务器。当前状态是: + +```text +本地发布包:已生成 +发布包扫描:通过 +完整 release gate:通过 +线上部署:尚未执行 +``` + +如果要由 Codex 继续实际发布,需要在下一步明确允许 SSH 发布操作。 + +## 手动发布命令清单 + +已生成: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-manual-deploy-commands-20260708.md +``` + +该文档包含: + +- `deploy/deploy-production.sh` 一键发布方式。 +- tar.gz 发布包上传方式。 +- 线上 SHA256 校验。 +- 线上备份、解包、Docker 重建。 +- 发布后 `/health`、`/app/`、`/app-v2/` 验证。 +- 宝塔专项验证。 +- 回滚命令。 + +## 最新最终验证 + +发布包扫描: + +```text +size_mb=27.02 +entries=2409 +OK: release bundle looks clean +``` + +完整 Gate: + +```text +60 passed in 1.70s +[release-gate] OK +``` + +### 方案 B:只同步核心文件到现有线上目录 + +适合快速热修: + +1. 同步后端核心文件。 +2. 同步 `web/app-v2/`。 +3. 重启后端服务。 +4. 验证宝塔一键登录和 `/app-v2/`。 + +缺点:如果线上依赖、静态目录或 Dockerfile 状态与本地不一致,容易遗漏。 + +## 线上验证清单 + +发布到: + +```text +https://api.synaglobal.vip +``` + +后验证: + +1. `/app/` 旧后台仍可访问。 +2. `/app-v2/` 新后台可访问。 +3. `/api/btpanel/servers/{id}/login-url` 正常返回登录地址。 +4. 同一台宝塔短时间重复点一键登录,应该只允许一个签发流程,冲突时返回 HTTP 409。 +5. 新增宝塔或配置缺失宝塔,在一键登录前会触发 session cleanup TTL 兜底检测/修复。 +6. 多台宝塔同时打开,2 小时后刷新不应因为 `task.py hardcoded_3600` 陆续掉线。 +7. 线上页面不显示全局 API Key 明文。 + +## 回滚点 + +发布前建议备份: + +```text +server/application/services/btpanel_service.py +server/infrastructure/btpanel/bootstrap_lock.py +server/infrastructure/btpanel/login_url_lock.py +server/infrastructure/btpanel/ssh_bootstrap.py +server/main.py +web/app-v2/ +Dockerfile +Dockerfile.prod +``` + +如果发布后宝塔一键登录异常: + +1. 先回滚上述后端文件。 +2. 重启后端服务。 +3. 保留 `/app-v2/` 不影响旧 `/app/`,必要时可以只移除 `web/app-v2/` 静态目录或取消 `server/main.py` 挂载。 + + +## 2026-07-08 validate-only verification + +The server-side release applier now supports validate-only mode. + +Command verified locally with Git Bash: + +```bash +bash deploy/apply-release-bundle.sh --validate-only --tarball /c/Users/uzuma/Documents/Codex/2026-07-06/yuu/outputs/nexus-release-20260708.tar.gz --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +` + +Result: + +` ext +SHA256 OK: 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +Extracted release tree validation OK +Validate-only mode: tarball checksum and content validation passed +` + +Network/SSH production execution is still not performed because Codex approval auto-review returned HTTP 404 for read-only checks. + + +## 2026-07-08 one-command local artifact preparation + +A single local command now prepares and verifies the release artifact end-to-end: + +`cmd +.venv-py312-codex\Scripts\python.exe scripts\prepare_release_artifact.py --old-snapshot C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --output-dir C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs --name nexus-release-20260708 +` + +Verified result: + +` ext +[prepare-release-artifact] OK +added=381 modified=26 deleted=347 +files=2409 +entries=2409 +size_mb=27.02 +sha256=46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +60 passed in 1.70s +` + + +## 2026-07-08 release index and checksum file + +Release index: + +` ext +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-index-20260708.md +` + +Standard SHA256 checksum file: + +` ext +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz.sha256 +` + +The checksum file contains: + +` ext +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b nexus-release-20260708.tar.gz +` + + +## Server preflight command + +Before applying the release on the production host, run: + +```bash +bash /tmp/nexus-release/deploy/preflight-release-host.sh \ + --tarball /tmp/nexus-release-20260708.tar.gz \ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \ + --deploy-path /opt/nexus +``` + +This checks required commands, checksum, release tar paths, deploy path, runtime hints, disk space, and current local endpoints before applying changes. diff --git a/docs/release/20260708/Nexus-release-text-integrity-gate-20260708.md b/docs/release/20260708/Nexus-release-text-integrity-gate-20260708.md new file mode 100644 index 00000000..15ac22e2 --- /dev/null +++ b/docs/release/20260708/Nexus-release-text-integrity-gate-20260708.md @@ -0,0 +1,52 @@ +# Nexus 发布文本完整性 Gate 增量记录(2026-07-08) + +## 背景 + +本轮在发布文档中发现隐藏控制字符 `0x08`,会把 Markdown 代码块显示成异常的 `bash` fence。该问题容易由 Windows PowerShell/终端编码或临时脚本拼接引入,肉眼不容易发现。 + +## 已修复 + +1. 新增源码检查脚本: + - `scripts/check_text_integrity.py` +2. 接入本地发布 Gate: + - `scripts/run_local_release_gate.py` +3. 清理文档隐藏字符: + - `outputs/*.md` 中的 `0x08` 已清除。 + - `docs/research/jumpserver-easynode-architecture-analysis.md` 中真实 ANSI ESC 已替换为可读的 `\u001b`。 +4. 重新生成发布包: + - `nexus-release-20260708.tar.gz` + - SHA256: `46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b` + - entries: `2407` + +## Gate 覆盖 + +`check_text_integrity.py` 会检查: + +- UTF-8 解码异常; +- 隐藏控制字符,允许 `\n`、`\r`、`\t`; +- 典型坏 fence:真实 backspace 混入 bash 代码块; +- 自动跳过旧后台构建产物和 vendor 目录,避免误报 xterm/Monaco 的合法控制码表。 + +## 验证结果 + +- 文本完整性检查:`OK: scanned 1751 text file(s)` +- `/app-v2` build:safe:通过 +- app-v2 sensitive scan:通过 +- 宝塔专项 pytest:`60 passed` +- 发布包扫描:通过 +- 发布包 validate-only:通过 +- metadata / `.sha256` / 实际 tar SHA:一致 + +## 当前最新发布包 + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b nexus-release-20260708.tar.gz +``` + +## 线上状态 + +截至本记录生成时,线上 `https://api.synaglobal.vip` 尚未由 Codex 完成发布验证;原因仍是本机 Codex 审批代理 `codex-auto-review` 不可用导致联网/SSH 操作被拒绝。 diff --git a/docs/release/20260708/Nexus-代码上下文库与Git基线报告-20260707.md b/docs/release/20260708/Nexus-代码上下文库与Git基线报告-20260707.md new file mode 100644 index 00000000..a4efeccc --- /dev/null +++ b/docs/release/20260708/Nexus-代码上下文库与Git基线报告-20260707.md @@ -0,0 +1,174 @@ +# Nexus 代码上下文库与局域网 Git 基线报告 + +生成时间:2026-07-07 07:18(Asia/Shanghai) + +## 1. 当前结论 + +已经完成两件事: + +1. 在局域网 Linux 测试机 `/opt/nexus-dev-current` 初始化了本地 Git 仓库。 +2. 在本地 Codex 工作区生成了 Nexus JSONL 代码上下文库和第一版安全审查入口报告。 + +没有使用 SQLite。 + +## 2. 局域网 Linux Git 状态 + +测试机:`192.168.124.219` + +Nexus 目录: + +```text +/opt/nexus-dev-current -> /opt/nexus-dev-20260707-054832 +``` + +Git 仓库实际位置: + +```text +/opt/nexus-dev-20260707-054832/.git +``` + +基线提交: + +```text +9d42fcd chore: baseline Nexus LAN test deployment snapshot +``` + +当前分支: + +```text +audit/security-review +``` + +当前状态: + +```text +clean +``` + +## 3. Git 防误提交保护 + +已配置 `.git/info/exclude` 本地排除规则,防止真实运行 secrets 和本地凭据误入库。 + +已排除重点包括: + +```text +.env +.env.* +docker/.env +docker/.env.* +*.pem +*.key +*.p12 +*.pfx +*resolved* +*凭据* +机器与凭据.md +ssh-config.resolved.txt +nexus-1panel.secrets.resolved.sh +nexus-1panel.secrets.sh +SECRETS.md +.git-askpass-nexus.sh +scripts/npm-proxy.env +*.bak +*.bak-* +node_modules/ +.venv/ +venv/ +__pycache__/ +.pytest_cache/ +``` + +提交前已检查高危敏感文件名,没有发现 `.env`、pem/key、resolved secrets 等被跟踪。 + +## 4. 本地代码上下文库 + +本地源代码快照: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 +``` + +本地上下文库: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context +``` + +输出副本: + +```text +outputs/nexus-code-context-20260707 +``` + +## 5. 索引统计 + +| 项 | 数量 | +|---|---:| +| files | 2577 | +| text_files | 2103 | +| symbols | 2823 | +| api_routes | 214 | +| service_methods | 319 | +| call_edges | 16594 | +| db_queries | 433 | +| redis_keys | 725 | +| ssh_commands | 343 | +| btpanel_flows | 172 | +| frontend_routes | 31 | +| secret_refs | 4425 | +| grep_hits | 8533 | +| chunks | 1312 | + +## 6. 已生成的核心报告 + +```text +reports/00-overview.md +reports/01-api-routes.md +reports/02-service-methods.md +reports/03-btpanel-login-chain.md +reports/04-ssh-risk-map.md +reports/05-redis-lock-map.md +reports/06-db-query-map.md +reports/07-security-review-entrypoints.md +``` + +## 7. 已生成的 JSONL 索引 + +```text +index/files.jsonl +index/symbols.jsonl +index/api_routes.jsonl +index/service_methods.jsonl +index/call_edges.jsonl +index/db_queries.jsonl +index/redis_keys.jsonl +index/ssh_commands.jsonl +index/btpanel_flows.jsonl +index/frontend_routes.jsonl +index/secrets_inventory.jsonl +``` + +## 8. 后续建议 + +下一步建议按这个顺序开始真正安全审查: + +1. 先审 `03-btpanel-login-chain.md`,确认宝塔一键登录链路和长期掉线边界。 +2. 再审 `04-ssh-risk-map.md`,确认 SSH/远程命令拼接、quote、timeout、日志脱敏。 +3. 再审 `01-api-routes.md` 和 `07-security-review-entrypoints.md`,确认敏感 API 是否都有鉴权。 +4. 最后按 `05-redis-lock-map.md` 和 `06-db-query-map.md` 审 Redis 锁/TTL 和 DB 查询。 + +后续所有代码修改建议在测试机分支执行: + +```bash +git status +git diff +git add +git commit -m "fix: ..." +``` + +修复后继续跑: + +```bash +python -m pytest -q +``` + diff --git a/docs/release/20260708/Nexus-安全巡检阶段4-settings-auth-btpanel-20260707.md b/docs/release/20260708/Nexus-安全巡检阶段4-settings-auth-btpanel-20260707.md new file mode 100644 index 00000000..635901d5 --- /dev/null +++ b/docs/release/20260708/Nexus-安全巡检阶段4-settings-auth-btpanel-20260707.md @@ -0,0 +1,368 @@ +# Nexus 安全巡检阶段 4:Settings/Auth 与宝塔长期会话边界审查 + +时间:2026-07-07 +对象:测试机 `/opt/nexus-dev-current`,分支 `audit/security-review` +最新提交:`524acd5 fix-stop-exposing-global-api-key-in-settings` + +> 本报告不输出任何密码、API Key、Token、Cookie、PEM 内容。 + +--- + +## 1. 本轮确认结果 + +### 1.1 全局 API Key 不显示:已完成代码层修复 + +只读检查结果: + +- 后端 `/api/settings/api-key/reveal` 已删除。 +- 前端 SettingsPage 中全局 API Key 显示/复制逻辑已删除。 +- 源码残留仅有: + - `server/api/servers.py` 两处注释仍提到旧名字 `reveal_api_key`。 + - `frontend/src/utils/auditLabels.ts` 仍有旧审计 label `reveal_api_key: 查看 API 密钥`。 + +安全判断: + +- 残留注释/label 不会造成 API Key 暴露。 +- 建议后续做一次小清理,避免误导维护者和审计日志翻译。 + +### 1.2 当前代码没有全局 API 限速 + +沿用上一份报告结论: + +- 没有 slowapi / fastapi-limiter / nginx limit_req 一类全局限速。 +- 429 主要来自: + - 管理员登录失败锁定:5 次 / 15 分钟。 + - Agent 脚本回调:job 10 次 / 60 秒,IP 60 次 / 60 秒。 +- 大多数“速度限制”是远程任务并发保护,不是 HTTP API 限速。 + +--- + +## 2. 宝塔 2 小时后刷新掉线:当前代码仍有一个关键边界 + +### 2.1 当前一键登录链路 + +```mermaid +sequenceDiagram + participant UI as Nexus 前端 + participant API as /api/btpanel/servers/{id}/login-url + participant SVC as BtPanelService.create_login_url + participant Redis as Redis login_url_lock + participant SSH as ssh_bootstrap_panel + participant BT as 宝塔面板 + + UI->>API: 请求一键登录 URL + API->>SVC: create_login_url(server_id) + SVC->>Redis: 获取每台服务器分布式锁 + SVC->>SSH: best-effort 检查/patch 宝塔 task.py session cleanup + SVC->>BT: config?action=set_temp_login(expire_time=now+86400) + BT-->>SVC: 返回 tmp_token 或登录 URL + SVC-->>UI: /login?tmp_token=... +``` + +### 2.2 已修复的问题 + +当前代码已经解决: + +1. `tmp_token` 使用 86400 秒 TTL,而不是宝塔默认较短 TTL。 +2. 登录 URL 不缓存,因为 `tmp_token` 是一次性链接。 +3. 对同一台服务器生成登录 URL 时使用 Redis 锁,避免并发覆盖。 +4. 登录前会 best-effort patch 宝塔 `/www/server/panel/task.py`:把硬编码 `if f_time > 3600:` 改成读取 `public.get_session_timeout() or 86400`。 + +### 2.3 仍可能导致“2 小时后刷新掉线”的关键点 + +当前 patch 的语义是: + +```python +session_cleanup_timeout = int(public.get_session_timeout() or 86400) +if f_time > session_cleanup_timeout: + 删除 session 文件 +``` + +这意味着: + +- 如果宝塔面板里已经配置了 session timeout,例如 7200 秒(2 小时),patch 会尊重这个值。 +- `or 86400` 只在没有配置值时生效。 +- 所以用户遇到“打开 10 个宝塔,2 小时后陆续刷新掉线”,仍可能是目标宝塔的 `session_timeout` 本身就是 7200 秒。 + +结论: + +> 现在的修复主要解决 `task.py` 硬编码 3600 秒清理问题,但不一定强制把宝塔会话生命周期提升到 24 小时。如果目标面板配置仍是 2 小时,会继续 2 小时后掉线。 + +### 2.4 并发打开 10 个宝塔的边界 + +当前 Redis 锁可以保证同一台服务器不会同时生成多个互相覆盖的 `tmp_token`。但用户说的是“同时打开 10 个不同宝塔”,这属于不同 server_id: + +- 每台服务器独立锁,不互相等待。 +- 各自的 session 生命周期取决于各自宝塔面板配置和 task.py patch 状态。 +- 10 台里有的先掉、有的后掉,符合“每台服务器 session_timeout/task.py/定时清理状态不一致”的表现。 + +--- + +## 3. Settings/Auth 发现的问题与建议 + +### 3.1 中风险:Telegram Token reveal 不需要二次认证 + +位置: + +- `server/api/settings.py` `/telegram/reveal-token` +- `server/api/settings.py` `/telegram/offline/reveal-token` + +现状: + +- 需要 JWT。 +- 会写审计日志。 +- 但不像密码预设 reveal、SSH Key reveal 那样要求当前密码二次认证。 + +风险: + +- 如果管理员 access token 被盗,在 token 有效期内可直接读取 Telegram Bot Token。 +- 虽然这是管理员功能,但与同文件中“密码预设/SSH 私钥 reveal 需要二次认证”的策略不一致。 + +建议: + +- 若继续允许显示 Telegram Token,建议改成 `current_password` 二次认证。 +- 如果你倾向于“全局 API Key 一样不显示”,也可以直接删除 Telegram Token reveal,只允许覆盖写入。 + +### 3.2 中风险:TOTP setup/enable 不要求当前密码 + +位置: + +- `server/api/auth.py` `/totp/setup` +- `server/api/auth.py` `/totp/enable` + +现状: + +- 需要 JWT。 +- 只允许对自己操作。 +- enable 前会校验 TOTP code。 +- 但不要求当前密码。 + +风险: + +- 如果 access token 被盗,攻击者可以生成自己的 TOTP secret 并启用,造成账号被锁或持久化控制风险。 + +建议: + +- setup 或 enable 至少其中一步要求当前密码。 +- 更严格方案:setup、enable、disable、password change 都统一走“敏感操作二次认证”。 + +### 3.3 低风险:全局 API Key 相关审计 label/注释残留 + +位置: + +- `server/api/servers.py` 注释里仍有 `same pattern as reveal_api_key`。 +- `frontend/src/utils/auditLabels.ts` 仍有 `reveal_api_key: 查看 API 密钥`。 + +风险: + +- 不暴露 secret。 +- 但会误导后续开发,以为还有 reveal_api_key 功能。 + +建议: + +- 后续清理为通用描述,例如 `same pattern as sensitive operation re-auth`。 +- 删除前端旧 label 或保留但标注 legacy。 + +### 3.4 低风险:refresh 接口仍接受 body refresh_token fallback + +位置: + +- `server/api/auth.py` `/api/auth/refresh` + +现状: + +- 优先读取 HttpOnly cookie。 +- 没有 cookie 时允许从 body 读取 refresh_token。 + +风险: + +- 当前注释解释为兼容 fallback。 +- 如果没有明确的历史客户端需要,body fallback 增加了一条 token 传输路径。 + +建议: + +- 保守:保留但加审计/废弃期。 +- 严格:只允许 HttpOnly cookie refresh。 + +--- + +## 4. 下一步修改方向:至少 6 个方案供选择 + +> 按你的要求,涉及安全策略/代码改动前先给多个选项,不直接替你定。 + +### 方案 A:只修宝塔长期会话,强制把宝塔 session timeout 提升到 24 小时 + +改动: + +- 在 `ssh_bootstrap.py` 里增加写入/修正宝塔 session timeout 配置的逻辑。 +- 登录前 best-effort 确保目标面板的 session timeout >= 86400。 +- 保留现有 task.py patch。 + +好处: + +- 最直接解决“2 小时后刷新掉线”。 +- 不影响 Nexus 自身安全策略。 + +风险: + +- 会修改宝塔面板自身配置。 +- 如果用户希望宝塔短会话,这会改变预期。 + +是否需要重启/部署: + +- 需要重建/重启 Nexus 后端容器。 +- 对目标宝塔可能需要 reload bt。 + +推荐度:高,针对当前痛点最直接。 + +### 方案 B:宝塔 session timeout 只做检测和告警,不自动修改 + +改动: + +- Nexus 登录前 SSH 检测宝塔当前 session timeout。 +- 如果低于 86400,在 UI 或接口响应中返回 warning。 +- 不自动改宝塔。 + +好处: + +- 风险低,不擅自改目标面板。 +- 可解释每台为什么 2 小时掉。 + +风险: + +- 不自动解决问题,仍需要人工确认/点击修复。 + +推荐度:中。 + +### 方案 C:给宝塔登录 URL 接口增加“会话保活/会话诊断”返回字段 + +改动: + +- `/api/btpanel/servers/{id}/login-url` 返回: + - `session_cleanup_ttl_ok` + - `session_cleanup_ttl_patched` + - `session_timeout_seconds` + - `warnings` +- 前端一键登录时提示“当前宝塔会话约 2 小时后失效”。 + +好处: + +- 用户能看到问题原因。 +- 便于排查 10 台里哪台会掉。 + +风险: + +- 需要补前端显示。 +- 只诊断,不一定修复。 + +推荐度:中高,适合和 A/B 搭配。 + +### 方案 D:敏感 Token 全部不显示,只允许覆盖写入 + +改动: + +- 删除 Telegram Token reveal。 +- 保留 token 是否已设置状态。 +- 修改前端:显示“已设置,留空不改”,不提供查看/复制。 + +好处: + +- 和“全局 API Key 不显示”策略一致。 +- 降低 access token 被盗后的进一步泄露面。 + +风险: + +- 管理员无法从 Nexus 找回 Telegram Token,只能重填。 + +推荐度:中高,安全更干净。 + +### 方案 E:保留 Telegram/密码/SSH reveal,但统一要求二次认证 + +改动: + +- Telegram reveal 请求体增加 `current_password`。 +- 前端弹窗输入当前密码。 +- 可以同时把 reveal 审计统一标准化。 + +好处: + +- 兼顾可用性和安全。 +- 与密码预设、SSH Key reveal 保持一致。 + +风险: + +- 使用上多一步。 + +推荐度:高,如果你还想保留“查看 Telegram Token”。 + +### 方案 F:统一敏感操作二次认证框架 + +改动范围更大: + +- TOTP setup/enable/disable。 +- 修改密码。 +- reveal 密码预设/SSH Key/Telegram Token。 +- 宝塔 API Key 修改/显示类操作。 +- 服务器凭据 reveal/更新。 + +好处: + +- 安全策略统一,不会每个接口散落实现。 +- 后续审计更清晰。 + +风险: + +- 改动较大,测试量更大。 +- 前端交互也要统一。 + +推荐度:长期推荐,短期可分阶段做。 + +### 方案 G:refresh token 只允许 HttpOnly Cookie,不再接受 body fallback + +改动: + +- `/api/auth/refresh` 删除 body refresh_token fallback。 +- 如有历史客户端,先加配置开关或废弃日志。 + +好处: + +- Token 传输路径更少。 +- 更符合当前注释里的安全目标。 + +风险: + +- 如果有脚本/旧前端依赖 body refresh,会受影响。 + +推荐度:中,先确认没有兼容需求再做。 + +--- + +## 5. 我建议的执行顺序 + +如果目标是先解决“宝塔 2 小时掉线”,建议: + +1. 先做方案 C 的诊断字段或 SSH 检测脚本,确认目标面板 session timeout 是否为 7200。 +2. 再做方案 A:自动把宝塔 session timeout 提升到 86400。 +3. 回归测试:同一台宝塔一键登录后刷新不掉;多台并发生成登录 URL 不互相覆盖。 +4. 然后再做 settings/auth 安全收口:优先方案 D 或 E。 + +如果目标是先收安全面,建议: + +1. 方案 D 或 E 处理 Telegram Token。 +2. 方案 F 分阶段统一敏感操作二次认证。 +3. 方案 G 作为兼容性确认后的增强项。 + +--- + +## 6. 待用户选择 + +请在以下方向里选一个或多个: + +1. A:自动强制宝塔 session timeout >= 86400,优先解决掉线。 +2. B:只检测告警,不自动改宝塔。 +3. C:一键登录返回会话诊断字段,前端提示原因。 +4. D:Telegram Token 像全局 API Key 一样完全不显示。 +5. E:Telegram Token 继续可显示,但必须当前密码二次认证。 +6. F:做统一敏感操作二次认证框架。 +7. G:refresh token 只允许 HttpOnly Cookie。 + diff --git a/docs/release/20260708/Nexus-宝塔Session自动修复正式发布-20260707.md b/docs/release/20260708/Nexus-宝塔Session自动修复正式发布-20260707.md new file mode 100644 index 00000000..9a5a7bcb --- /dev/null +++ b/docs/release/20260708/Nexus-宝塔Session自动修复正式发布-20260707.md @@ -0,0 +1,224 @@ +# Nexus 宝塔 Session 自动修复正式发布记录 + +日期:2026-07-07 +正式域名:`https://api.synaglobal.vip` +正式主机:`20.24.218.235` +正式容器:`nexus-prod-nexus-1` +测试提交:`5ac3f56 fix: auto repair btpanel session cleanup ttl` + +## 发布目标 + +把“新增宝塔自动检测/修复 hardcoded_3600”与“一键登录前兜底检测/修复”发布到正式环境,解决宝塔 tmp_login session 被 `/www/server/panel/task.py` 固定 3600 秒清理导致一键登录后 1~2 小时刷新掉线的问题。 + +## 本次正式发布范围 + +发布方式:`docker cp` 热更新正式容器内文件,然后重启容器。 +说明:正式机 `/opt/nexus` 工作树较脏,本次没有执行 `git reset` / `git pull`,避免覆盖线上已有改动。 + +覆盖正式容器内文件: + +- `/app/server/infrastructure/btpanel/ssh_bootstrap.py` +- `/app/server/infrastructure/btpanel/bootstrap_state.py` +- `/app/server/application/services/btpanel_service.py` + +来源文件本地留存: + +- `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\prod-release-20260707-bt-session\ssh_bootstrap.py` +- `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\prod-release-20260707-bt-session\bootstrap_state.py` +- `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\prod-release-20260707-bt-session\btpanel_service.py` + +正式机 staging 目录: + +- `/tmp/nexus_bt_session_release_20260707/` + +正式机备份目录: + +- `/tmp/nexus_bt_session_backup_20260707_194326/` + +## 已发布能力 + +### 1. 新增宝塔 / bootstrap 阶段自动修复 + +在远程宝塔服务器上检查: + +- `/www/server/panel/task.py` + +如果发现旧版精确模式: + +```python +if f_time > 3600: +``` + +则自动执行: + +1. 备份原文件为 `/www/server/panel/task.py.nexus-session-ttl.bak`; +2. 改为读取宝塔自身 session timeout 配置: + +```python +session_cleanup_timeout = int(public.get_session_timeout() or 86400) +if f_time > session_cleanup_timeout: +``` + +3. 保留原文件权限; +4. 写入前执行 `compile(...)` 与 `py_compile.compile(..., doraise=True)`; +5. 通过临时文件 + `os.replace(...)` 原子替换; +6. 变更后执行 `/etc/init.d/bt reload`。 + +如果 task.py 结构不是已知安全模式,则记录 `unknown_task_pattern`,不做模糊替换,避免误改宝塔代码。 + +### 2. 一键登录前兜底检测/修复 + +`BtPanelService.create_login_url()` 生成一键登录 URL 前会检查已配置宝塔 API 的服务器: + +- 成功修复/确认后:24 小时内不重复检查; +- 失败后:1 小时后允许重试; +- 兜底检查失败不会阻断本次一键登录,只会记录 diagnostics 和状态字段。 + +### 3. 状态字段 + +状态写入:`servers.extra_attrs.bt_panel` + +新增/更新字段: + +- `session_cleanup_ttl_ok` +- `session_cleanup_ttl_patched` +- `session_cleanup_task_state` +- `session_cleanup_ttl_checked_at` +- `session_cleanup_ttl_last_error` +- `panel_ssl` + +`session_cleanup_task_state` 常见值: + +- `patched_timeout`:已修好或本次修好; +- `unknown_task_pattern`:存在 task.py,但结构不是可安全自动 patch 的已知模式; +- `no_panel`:未找到宝塔面板 task.py; +- `error`:检测/修复异常。 + +一键登录响应 diagnostics 会返回非敏感诊断字段,例如: + +```json +{ + "session_cleanup_ttl_ok": true, + "session_cleanup_ttl_patched": true, + "session_cleanup_task_state": "patched_timeout" +} +``` + +不会输出宝塔临时登录 token。 + +## 验证记录 + +### 测试部署机验证 + +测试部署机:`192.168.124.219` +项目目录:`/opt/nexus-dev-current` +分支:`audit/security-review` + +执行测试: + +```bash +.venv/bin/python -m pytest \ + tests/test_btpanel_ssh_bootstrap.py \ + tests/test_btpanel_login_url.py \ + tests/test_btpanel_bootstrap_loop.py \ + tests/test_btpanel_login_url_route.py \ + tests/test_btpanel_ssh_login.py \ + tests/test_btpanel_temp_login_ttl.py -q +``` + +结果: + +```text +39 passed in 0.61s +``` + +### 正式容器发布前检查 + +正式容器临时目录内 3 个 Python 文件编译通过: + +```bash +python -m py_compile \ + /tmp/nexus_bt_session_release_20260707/ssh_bootstrap.py \ + /tmp/nexus_bt_session_release_20260707/bootstrap_state.py \ + /tmp/nexus_bt_session_release_20260707/btpanel_service.py +``` + +### 正式容器覆盖后检查 + +覆盖到 `/app/server/...` 后再次编译通过: + +```bash +python -m py_compile \ + /app/server/infrastructure/btpanel/ssh_bootstrap.py \ + /app/server/infrastructure/btpanel/bootstrap_state.py \ + /app/server/application/services/btpanel_service.py +``` + +代码关键字确认已生效: + +- `unknown_task_pattern` +- `py_compile.compile(tmp, doraise=True)` +- `session_cleanup_task_state` + +### 正式健康检查 + +正式容器重启后: + +```text +nexus-prod-nexus-1 Up / healthy +http://127.0.0.1:8600/health => ok +https://api.synaglobal.vip/health => ok +``` + +## 安全边界 + +本次没有做以下高风险动作: + +- 不显示、不保存、不输出宝塔 `tmp_token`; +- 不输出 API Key、JWT、Cookie、密码、私钥; +- 不自动关闭宝塔 `check_client_hash()`; +- 不自动修改宝塔面板 SSL 开关; +- 不对未知 task.py 结构做模糊替换; +- 不对正式机代码仓库做 `git reset` / `git pull`; +- 不批量重启用户业务服务器,只在需要 patch 宝塔 task.py 时 reload 宝塔面板。 + +## 回滚方式 + +如需回滚本次正式热更新,可在正式主机执行以下思路: + +```bash +# 1. 从备份目录恢复 3 个文件到容器 +sudo docker cp /tmp/nexus_bt_session_backup_20260707_194326/ssh_bootstrap.py \ + nexus-prod-nexus-1:/app/server/infrastructure/btpanel/ssh_bootstrap.py + +sudo docker cp /tmp/nexus_bt_session_backup_20260707_194326/bootstrap_state.py \ + nexus-prod-nexus-1:/app/server/infrastructure/btpanel/bootstrap_state.py + +sudo docker cp /tmp/nexus_bt_session_backup_20260707_194326/btpanel_service.py \ + nexus-prod-nexus-1:/app/server/application/services/btpanel_service.py + +# 2. 编译检查 +sudo docker exec nexus-prod-nexus-1 python -m py_compile \ + /app/server/infrastructure/btpanel/ssh_bootstrap.py \ + /app/server/infrastructure/btpanel/bootstrap_state.py \ + /app/server/application/services/btpanel_service.py + +# 3. 重启并检查健康 +sudo docker restart nexus-prod-nexus-1 +curl -sf http://127.0.0.1:8600/health +``` + +## 后续建议 + +1. 后续新增宝塔:bootstrap 阶段会自动检测/修复 hardcoded_3600。 +2. 老的已绑定宝塔:用户点击一键登录前会兜底检测/修复。 +3. 如果某台机器 diagnostics 显示 `unknown_task_pattern`,需要单独人工看那台宝塔的 `/www/server/panel/task.py`,不要自动硬替换。 +4. 如果用户仍反馈“刷新后掉线”,优先检查该服务器的 diagnostics: + - `session_cleanup_task_state` + - `session_cleanup_ttl_ok` + - `session_cleanup_ttl_last_error` + - 面板 SSL / 浏览器混合内容 / Cookie SameSite 等。 + +## 结论 + +正式环境 `https://api.synaglobal.vip` 已发布“新增宝塔自动检测/修复 hardcoded_3600”和“一键登录前兜底检测/修复”。当前正式健康检查通过,测试机相关用例全部通过。后续新增宝塔和老宝塔的一键登录入口都会自动进入这套检测/修复流程。 \ No newline at end of file diff --git a/docs/release/20260708/Nexus-宝塔hardcoded3600批量修复完成-20260707.md b/docs/release/20260708/Nexus-宝塔hardcoded3600批量修复完成-20260707.md new file mode 100644 index 00000000..cc9bb853 --- /dev/null +++ b/docs/release/20260708/Nexus-宝塔hardcoded3600批量修复完成-20260707.md @@ -0,0 +1,132 @@ +# Nexus 宝塔 `hardcoded_3600` 批量修复完成记录(2026-07-07) + +## 修复范围 + +来源:`work/bt_bulk_session_audit.before_patch_20260707-1838.jsonl`。 + +本次只处理: + +- 在线服务器 +- `task_state=hardcoded_3600` +- 共 148 台 + +本次没有处理: + +- `panel_ssl=0` 的 112 台 +- `unknown_task_pattern` 的 268 台 +- 其它异常/无面板机器 + +## 修复动作 + +对每台目标服务器执行: + +1. 检查 `/www/server/panel/task.py`。 +2. 查找硬编码逻辑: + - `if f_time > 3600:` +3. 在 session 清理循环前插入: + - 默认 `session_cleanup_timeout = 86400` + - 优先读取 `public.get_session_timeout()` +4. 把条件改成: + - `if f_time > session_cleanup_timeout:` +5. 写入前做备份: + - `/www/server/panel/task.py.nexus-session-ttl.bak` +6. 写入临时文件并执行 Python 编译检查。 +7. 原子替换 `task.py`。 +8. 如果本次确实发生修改,则执行 `/etc/init.d/bt reload`。 + +## 执行批次 + +第一批 10 台:执行 ID `5944`。 + +剩余 138 台按 20 台一批执行: + +| 批次 | 执行 ID | 数量 | 结果 | +|---:|---:|---:|---| +| 1 | 5944 | 10 | 成功 | +| 2 | 5945 | 20 | 成功 | +| 3 | 5946 | 20 | 成功 | +| 4 | 5947 | 20 | 成功 | +| 5 | 5948 | 20 | 成功 | +| 6 | 5949 | 20 | 成功 | +| 7 | 5950 | 20 | 成功 | +| 8 | 5951 | 18 | 成功 | + +总计:148 / 148 成功。 + +## 执行结果 + +所有目标机器返回: + +- `exit_code=0` +- `patch_result=patched` +- `after_patched=True` + +本地结果文件: + +- `work/bt_session_ttl_patch_batch_20260707-183535.json` +- `work/bt_session_ttl_patch_batch_20260707-183648_exec5945.json` +- `work/bt_session_ttl_patch_batch_20260707-183658_exec5946.json` +- `work/bt_session_ttl_patch_batch_20260707-183707_exec5947.json` +- `work/bt_session_ttl_patch_batch_20260707-183717_exec5948.json` +- `work/bt_session_ttl_patch_batch_20260707-183727_exec5949.json` +- `work/bt_session_ttl_patch_batch_20260707-183737_exec5950.json` +- `work/bt_session_ttl_patch_batch_20260707-183747_exec5951.json` +- `work/bt_session_ttl_patch_summary_20260707-183747.json` + +## 修复后全量只读复查 + +复查范围:在线 420 台。 + +复查结果: + +| 状态 | 数量 | +|---|---:| +| `patched_timeout` | 149 | +| `unknown_task_pattern` | 268 | +| `no_panel` | 1 | +| `error` | 2 | +| `hardcoded_3600` | 0 | + +关键验证: + +- 修复前 `hardcoded_3600`:148 台 +- 修复后这 148 台全部变为:`patched_timeout` +- 修复后剩余 `hardcoded_3600`:0 台 + +全量复查中有 2 台轮询超时/解析异常: + +- server_id `225` +- server_id `1` + +这 2 台不是本轮 `hardcoded_3600` 未修复目标;本轮 148 台目标已全部验证成功。 + +## 当前剩余问题 + +仍有 112 台 `panel_ssl=0`,它们主要是另一类问题: + +- 多数为 Ubuntu 24.04.4 +- 多数为腾讯云 `VM-*` 主机名 +- 多数属于 `unknown_task_pattern` +- 不适合直接套本轮 `hardcoded_3600` 补丁 + +建议下一步:对 112 台 `panel_ssl=0` 抽样 5-10 台读取 `task.py` / `public.py`,按宝塔版本分类后再决定策略。 + +## 回滚方式 + +单台回滚: + +```bash +cp -a /www/server/panel/task.py.nexus-session-ttl.bak /www/server/panel/task.py +python -m py_compile /www/server/panel/task.py +/etc/init.d/bt reload +``` + +如果宝塔使用独立 pyenv,编译命令可替换为: + +```bash +/www/server/panel/pyenv/bin/python3 -m py_compile /www/server/panel/task.py +``` + +## 结论 + +148 台 `hardcoded_3600` 已全部修复并通过复查,当前在线 420 台里 `hardcoded_3600` 已清零。 diff --git a/docs/release/20260708/Nexus-宝塔一键登录掉线深查-20260707.md b/docs/release/20260708/Nexus-宝塔一键登录掉线深查-20260707.md new file mode 100644 index 00000000..f314120f --- /dev/null +++ b/docs/release/20260708/Nexus-宝塔一键登录掉线深查-20260707.md @@ -0,0 +1,403 @@ +# Nexus 宝塔一键登录 2 小时掉线阶段诊断报告(2026-07-07) + +## 1. 本轮结论 + +针对服务器 `42.193.182.190`(Nexus server_id 约为 `395`)排查后,目前结论是: + +1. **Nexus 生成的一键登录临时 token TTL 是 24 小时,不是 2 小时。** +2. 目标宝塔最近生成的 `temp_login` 记录 `ttl_seconds` 为 `86399/86400` 秒。 +3. 宝塔面板登录超时配置文件 `/www/server/panel/data/session_timeout.pl` 缺失,按宝塔源码 `public.get_session_timeout()` 默认应为 `86400` 秒。 +4. 宝塔 `task.py` 已经是按 `public.get_session_timeout()` 清理 session,不再是旧的硬编码 3600 秒清理。 +5. 目标宝塔未开启面板 SSL:`/www/server/panel/data/ssl.pl` 缺失。 +6. 未开启 SSL 时,宝塔源码 `public.check_client_hash()` 会启用客户端 HASH 校验;一旦客户端 HASH 变化,宝塔会清空 session 并要求重新登录。这个更符合“同时打开 10 个宝塔,过一段时间刷新陆续掉线”的现象。 + +## 2. 已验证证据 + +### 2.1 最新 temp_login TTL + +实际表位置: + +```text +/www/server/panel/data/db/default.db +``` + +最新记录示例: + +```text +id=108 +state=0 +add_local=2026-07-07 13:56:57 +expire_local=2026-07-08 13:56:56 +ttl_seconds=86399 +seconds_left≈86377 +``` + +说明 Nexus 生成的临时登录有效期约等于 24 小时。 + +### 2.2 宝塔登录超时配置 + +目标机: + +```text +/www/server/panel/data/session_timeout.pl = missing +``` + +宝塔源码逻辑:文件缺失时默认: + +```text +86400 seconds = 24 hours +``` + +### 2.3 宝塔 task.py session 清理 + +目标机 `/www/server/panel/task.py` 已存在: + +```python +session_cleanup_timeout = 86400 +try: + session_cleanup_timeout = int(public.get_session_timeout() or 86400) +except Exception: + pass + +if f_time > session_cleanup_timeout: + os.remove(filename) +``` + +所以当前不是旧版每 1 小时清理 session 的问题。 + +### 2.4 宝塔 SSL/client_hash 风险 + +目标机: + +```text +missing ssl.pl +``` + +宝塔源码中: + +```python +def is_ssl(): + return os.path.exists(get_panel_path() + '/data/ssl.pl') + +def check_client_hash(): + if is_ssl(): return True + ... + if session[skey] != client_hash: + WriteLog('用户登录', '客户端HASH验证失败,已强制退出登录...建议开启面板SSL或关闭IP-HASH验证!') + return False +``` + +推断:如果浏览器/代理/出口 IP/UA/客户端特征变化,或者多个宝塔窗口处于不同访问链路,刷新时可能触发 client_hash 校验失败,被宝塔强制退出。 + +## 3. 本轮已修 Nexus 问题 + +发现当前 `POST /api/btpanel/servers/395/login-url` 曾返回 502: + +```text +ModuleNotFoundError: No module named 'public' +``` + +根因:Nexus SSH fallback 用系统 Python 或未完整设置宝塔源码路径执行临时登录脚本。 + +已修改文件: + +```text +/opt/nexus-dev-current/server/infrastructure/btpanel/ssh_login.py +``` + +提交: + +```text +fa44439 fix btpanel ssh temp login python env +``` + +改动要点: + +1. Python 优先级调整为: + - `/www/server/panel/pyenv/bin/python3` + - `/www/server/panel/pyenv/bin/python` + - `python3` +2. 临时登录脚本执行前: + - `os.chdir('/www/server/panel')` + - 加入 `/www/server/panel` + - 加入 `/www/server/panel/class` +3. 已热更新到运行中容器并重启。 +4. 验证: + - `tests/test_btpanel_login_url.py`: `6 passed` + - 一键登录接口再次返回 `200 OK` + +注意:当前容器是单文件热更新;源码已提交。如果后续重新创建容器但不 rebuild 镜像,热更新会丢失。建议下一次部署时执行正式 build。 + +## 4. 为什么仍可能“2 小时后刷新掉线” + +目前不是以下原因: + +- 不是 Nexus temp token 只有 2 小时。 +- 不是 `public.get_session_timeout()` 配置成 2 小时。 +- 不是 `task.py` 仍按 3600 秒删除 session。 + +更可能是: + +1. 宝塔未开启面板 SSL,`check_client_hash()` 生效。 +2. 多个宝塔窗口、代理链路、出口 IP、浏览器 UA 或客户端指纹发生变化。 +3. 宝塔刷新请求时发现当前 session 内保存的 client_hash 与本次请求不同,于是清 session。 +4. 宝塔日志里这类错误通常是“客户端 HASH 验证失败,已强制退出登录”。 + +## 5. 下一步修复方案(待选择) + +### 方案 A:开启宝塔面板 SSL(推荐优先验证) + +- 原理:宝塔源码 `if is_ssl(): return True`,开启 SSL 后跳过 client_hash 校验。 +- 好处:遵循宝塔自身逻辑,不改源码。 +- 风险:需要处理证书、访问协议、端口和浏览器信任问题。 +- 影响:可能需要 reload/restart 宝塔。 + +### 方案 B:Nexus 增加宝塔登录诊断提示 + +- 内容:一键登录返回/审计中记录 `ttl_seconds`、`panel_ssl`、`client_hash_risk` 等非敏感信息。 +- 好处:以后看到某台机器掉线,可直接确认风险点。 +- 风险:要严格避免返回 token/cookie/API key。 +- 影响:小改动。 + +### 方案 C:只对临时登录 session 放宽 client_hash + +- 内容:patch 宝塔源码,若 `session['tmp_login'] == True` 则跳过或降低 client_hash 校验。 +- 好处:针对 Nexus 一键登录最直接。 +- 风险:改宝塔源码,升级可能覆盖;安全性低于开启 SSL。 +- 影响:需要 reload/restart 宝塔。 + +### 方案 D:关闭宝塔 IP-HASH/client_hash 校验 + +- 好处:最直接。 +- 风险:全局安全性下降,不建议默认。 +- 影响:所有宝塔登录会话受影响。 + +### 方案 E:Nexus 自动定期生成新临时登录 URL + +- 内容:掉线时重新点一键登录;或前端提供刷新登录入口。 +- 好处:不改宝塔。 +- 风险:不能解决当前会话刷新被踢,只是降低恢复成本。 +- 影响:小到中。 + +### 方案 F:前端/代理层保活 + +- 内容:定时请求宝塔保持 session 活跃。 +- 好处:对 session_timeout 问题有帮助。 +- 风险:对 client_hash 失败基本无效;跨域限制多。 +- 影响:不推荐作为主方案。 + +### 方案 G:Nexus 代理式宝塔访问 + +- 内容:Nexus 托管/代理宝塔访问链路,减少客户端 HASH 变化。 +- 好处:长期可控。 +- 风险:大改;安全边界复杂。 +- 影响:开发量最大。 + +## 6. 当前推荐 + +推荐顺序: + +1. 先选 **方案 A:开启宝塔面板 SSL**,验证多开 10 个宝塔 2 小时后刷新是否仍掉线。 +2. 同时做 **方案 B:Nexus 诊断提示**,以后每台宝塔能直接看 TTL/SSL/client_hash 风险。 +3. 如果 A 不方便或仍失败,再考虑 **方案 C:仅对临时登录放宽 client_hash**。 + +## 7. 待办 + +- [ ] 用户确认选 A/B/C/D/E/F/G 哪个方向。 +- [ ] 若选 A:在测试机上开启面板 SSL 并记录回滚方式。 +- [ ] 若选 B:实现 Nexus 只读诊断,不返回敏感 token/cookie。 +- [ ] 若选 C/D:先备份宝塔源码,再 patch,并保留一键回滚脚本。 +- [ ] 正式重建 Nexus Docker 镜像,让 `fa44439` 固化进镜像。 + +## 8. 补充:get_client_hash 精确逻辑 + +后续只读查看 `/www/server/panel/class/public.py` 后确认: + +```python +def get_client_hash(): + is_tmp_login = session.get('tmp_login') + is_session_safe_mode = session.get('session_safe_mode') + if is_tmp_login or is_session_safe_mode: + client_hash = md5(request.remote_addr) + else: + ... +``` + +因此对 Nexus 一键登录这种 `tmp_login=True` 的会话,宝塔主要绑定的是: + +```text +request.remote_addr +``` + +也就是宝塔看到的客户端来源 IP。 + +这进一步解释了“同时打开多个宝塔,约 2 小时后刷新陆续掉线”:如果浏览器代理、网络出口、反代、CDN、VPN 或宝塔前面的转发链路导致 `request.remote_addr` 变化,宝塔刷新时会认为客户端 HASH 不一致,然后清空 session。 + +所以最优先验证项变为: + +1. 开启宝塔面板 SSL,让 `check_client_hash()` 直接返回 True。 +2. 或保证访问宝塔时来源 IP 固定。 +3. 或只对 Nexus 临时登录会话放宽 `tmp_login` 的 IP 绑定。 + +--- + +## 2026-07-07 诊断字段落地与验证 + +### 本次代码改动 + +已在 Nexus 后端增加宝塔一键登录的非敏感诊断字段 `diagnostics`: + +- `temp_login_ttl_seconds`:Nexus 申请宝塔临时登录链接的 TTL,当前为 `86400` 秒。 +- `session_cleanup_ttl_ok`:宝塔 `task.py` 会话清理阈值巡检/修复是否成功。 +- `session_cleanup_ttl_patched`:本次/最近一次 SSH bootstrap 是否修补过会话清理阈值。 +- `session_cleanup_ttl_checked_at`:最近一次巡检时间。 +- `session_cleanup_ttl_last_error`:最近一次巡检错误码,非敏感。 +- `panel_ssl`:宝塔面板是否启用面板 SSL(检测 `/www/server/panel/data/ssl.pl`),与 Nexus 的 `verify_ssl` 不是一个概念。 +- `client_hash_risk`:当 `panel_ssl=false` 时为 `true`,表示宝塔临时登录 session 仍有来源 IP 变化后被 `check_client_hash()` 清掉的风险。 + +### 改动文件 + +远端测试部署机 `/opt/nexus-dev-current`: + +- `server/application/services/btpanel_service.py` +- `server/infrastructure/btpanel/bootstrap_state.py` +- `server/infrastructure/btpanel/ssh_bootstrap.py` +- `tests/test_btpanel_login_url.py` + +### 安全边界 + +- 没有返回或记录 `api_key`、cookie、`tmp_token`、宝塔密码、SSH 密钥等敏感值。 +- `diagnostics` 只包含 TTL、布尔状态、时间、短错误码。 +- 审计日志里新增的也是同一份非敏感 `diagnostics`。 +- 没有直接修改宝塔 SSL 开关或宝塔源码;只是把已知风险显式化。 + +### 测试结果 + +在 Nexus 测试部署机执行: + +```text +python -m py_compile server/infrastructure/btpanel/ssh_bootstrap.py \ + server/infrastructure/btpanel/bootstrap_state.py \ + server/application/services/btpanel_service.py \ + tests/test_btpanel_login_url.py \ + tests/test_btpanel_ssh_bootstrap.py + +pytest tests/test_btpanel_login_url.py tests/test_btpanel_login_url_route.py tests/test_btpanel_ssh_bootstrap.py -q +# 24 passed + +pytest -q +# 748 passed, 1 skipped +``` + +### 部署验证 + +已热更新 Nexus 测试容器: + +```text +docker cp server/application/services/btpanel_service.py nexus-nexus-1:/app/server/application/services/btpanel_service.py +docker cp server/infrastructure/btpanel/bootstrap_state.py nexus-nexus-1:/app/server/infrastructure/btpanel/bootstrap_state.py +docker cp server/infrastructure/btpanel/ssh_bootstrap.py nexus-nexus-1:/app/server/infrastructure/btpanel/ssh_bootstrap.py +docker restart nexus-nexus-1 +curl http://127.0.0.1:18600/health +# ok +``` + +注意:当前是测试机容器热更新,正式固化仍建议 rebuild 镜像并按发布流程部署。 + +### 当前结论 + +代码层面已能区分: + +1. Nexus 一键登录 token TTL(24h)。 +2. 宝塔 session 清理阈值是否已修复到 24h。 +3. 宝塔面板 SSL 是否开启。 +4. 未开启面板 SSL 时,`tmp_login` 绑定来源 IP 导致“刷新后掉线”的风险。 + +下一步如果要彻底解决“多开 10 个宝塔,过 2 小时刷新陆续掉线”,优先建议在用户确认方案后处理宝塔侧:启用面板 SSL,或统一浏览器访问出口/反代来源 IP;不建议默认改宝塔 `check_client_hash()`。 + +### 宝塔官方文档补充 + +宝塔官方文档「面板基础设置」中说明:面板“超时时间”默认设置为 **24 小时**,可选 1小时、6小时、12小时、24小时、3天、7天;超过该时间未操作面板会自动退出登录。参考:https://docs.bt.cn/10.0/user-guide/config/panel-settings + +这与目标机源码 `public.get_session_timeout()` 在缺少 `session_timeout.pl` 时默认 86400 秒的结果一致,因此目前“约 2 小时刷新掉线”的主因仍更像是未开启面板 SSL 时的 `client_hash` 来源 IP 绑定,而不是正常超时时间到了。 + +## 2026-07-07 正式环境 `api.synaglobal.vip` 最小热更新记录 + +### 部署方式 + +正式主机 `/opt/nexus` 当前工作树存在大量本地修改和未跟踪前端构建文件,因此没有执行 `git pull`、`git reset` 或整仓覆盖。 + +本次采用最小风险热更新:只把测试机已验证通过的 3 个后端 Python 文件复制进正式容器 `nexus-prod-nexus-1`: + +- `/app/server/application/services/btpanel_service.py` +- `/app/server/infrastructure/btpanel/bootstrap_state.py` +- `/app/server/infrastructure/btpanel/ssh_bootstrap.py` + +正式容器旧文件已先备份到: + +```text +/tmp/nexus-bt-diag-backup-20260707-153452 +``` + +### 部署校验 + +容器内 `py_compile` 通过,容器已重启,健康检查通过: + +```text +container: nexus-prod-nexus-1 +health: ok +status: Up About a minute (healthy) +``` + +部署后 3 个文件在正式容器内的 SHA256: + +```text +eacf143745b14727fa66de99551e88e26c2f7ece3b4b0f82a071484da8b2dbd8 btpanel_service.py +3f84246b6b0635c3a8f74848fe606b23cac407e9a775feb729d9c0f9185ffb4a bootstrap_state.py +c4a2a1010392249dec8de68fc89def5935440df18c5c7c24a79be87763708806 ssh_bootstrap.py +``` + +### 正式接口验证 + +通过 `https://api.synaglobal.vip` 调用 `POST /api/btpanel/servers/395/login-url` 验证成功: + +```text +auth_status: HTTP/1.1 200 OK +login_url_status: HTTP/1.1 200 OK +method: api +bootstrapped: false +url_sanitized: https://42.193.182.190:31701/login?tmp_token=*** +``` + +正式接口返回的非敏感诊断字段: + +```json +{ + "client_hash_risk": false, + "panel_ssl": true, + "session_cleanup_ttl_checked_at": "2026-07-07T07:35:19Z", + "session_cleanup_ttl_last_error": null, + "session_cleanup_ttl_ok": true, + "session_cleanup_ttl_patched": false, + "temp_login_ttl_seconds": 86400 +} +``` + +说明: + +- `temp_login_ttl_seconds=86400`:Nexus 一键登录临时链接仍是 24 小时。 +- `panel_ssl=true` 且 `client_hash_risk=false`:当前正式记录里这台宝塔面板已检测到面板 SSL 开启,因此“未开启 SSL 导致 tmp_login 绑定来源 IP”的风险在这次返回里不成立。 +- `session_cleanup_ttl_ok=true`:宝塔会话清理阈值检查成功。 +- `session_cleanup_ttl_patched=false`:表示本次检查没有新改动 `task.py`;不是失败。是否已经被旧补丁覆盖,要结合 `session_cleanup_ttl_ok` 和目标机源码再判断。 + +### 风险与后续 + +本次只是正式容器热更新,不是镜像固化发布。后续如果 1Panel/Docker 重新构建或重新拉镜像,这 3 个文件可能被镜像内容覆盖;建议下一步按正式发布流程把提交 `b5aaf5a feat: add btpanel login diagnostics` 合入正式镜像构建链路。 + +当前现网诊断结果显示 42.193.182.190 这台宝塔的 `panel_ssl=true`、`client_hash_risk=false`,如果用户仍复现“多开 10 个宝塔,约 2 小时后刷新陆续掉线”,下一步应优先抓以下现场: + +1. 掉线前后重新调用 `login-url`,比较 `diagnostics` 是否变化。 +2. 在宝塔机上检查 session 文件创建/删除时间和 `request.remote_addr` 来源变化。 +3. 检查浏览器/代理/反代是否对不同标签页或不同时间切换出口。 +4. 检查宝塔自身是否还有其它 session 清理逻辑或安全插件清理逻辑。 +5. 检查 Nexus 返回的 `method` 是否一直为 `api`,是否在某些机器上触发 SSH bootstrap 或 fallback。 diff --git a/docs/release/20260708/Nexus-宝塔一键登录长期掉线修复方案与代码改进.md b/docs/release/20260708/Nexus-宝塔一键登录长期掉线修复方案与代码改进.md new file mode 100644 index 00000000..76f68f56 --- /dev/null +++ b/docs/release/20260708/Nexus-宝塔一键登录长期掉线修复方案与代码改进.md @@ -0,0 +1,987 @@ +# Nexus 宝塔一键登录长期掉线修复方案与代码改进 + +生成时间:2026-07-07(Asia/Shanghai) +项目路径:`C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus` +生产 API:`https://api.synaglobal.vip` +排查机器:`42.193.182.190` +Nexus server id:`395` + +> 本文档不包含任何密码、token、私钥、API key。所有敏感凭据均已省略。 + +--- + +## 1. 问题背景 + +用户反馈: + +- Nexus 一键登录宝塔本身能成功。 +- 同时打开多个宝塔面板后,约 1~2 小时刷新会陆续掉线。 +- 目标机器示例:`42.193.182.190`。 + +一开始怀疑点: + +1. Nexus 生成的宝塔临时登录 token TTL 太短。 +2. Nexus 一键登录 URL 缓存导致返回已消费的一次性 token。 +3. 多 worker / 多请求并发生成宝塔临时登录,互相覆盖或失效。 +4. 宝塔自身 session 清理策略导致登录态被清掉。 + +最终确认:**根因是第 4 点,宝塔自身 `BT-Task` 定期清理 session 文件时硬编码 3600 秒。** + +--- + +## 2. 排查结论 + +### 2.1 Nexus 生产生成的一键登录 token 已经是 24 小时 + +通过生产 Nexus 调用: + +```http +POST /api/btpanel/servers/395/login-url +``` + +返回: + +```text +method=api +url=https://42.193.182.190:31701/login?tmp_token=*** +``` + +随后在目标机宝塔 DB 查到最新 `temp_login`: + +```text +state=0 +ttl_seconds≈86399 +remain_seconds≈86398 +``` + +因此 Nexus 当前生产生成的宝塔临时登录不是 2/3 小时,而是约 **24 小时**。 + +### 2.2 目标机器宝塔配置也是 24 小时 + +目标机器:`42.193.182.190` + +```text +/www/server/panel/data/session_timeout.pl = 86400 +宝塔版本:11.0.0 +宝塔端口:31701 +``` + +### 2.3 真正根因:宝塔 task.py 硬编码清理 1 小时 session + +宝塔后台任务文件: + +```text +/www/server/panel/task.py +``` + +原始逻辑片段: + +```python +if f_time > 3600: + os.remove(filename) + continue +``` + +宝塔每 10 分钟跑一次 session 清理,超过 3600 秒的 session 文件会被删除。 + +临时登录刷新时又依赖 session 文件存在。相关逻辑在: + +```text +/www/server/panel/class/common.py +``` + +核心逻辑: + +```python +if 'tmp_login_expire' in session: + s_file = 'data/session/{}'.format(session['tmp_login_id']) + if session['tmp_login_expire'] < time.time(): + session.clear() + if os.path.exists(s_file): + os.remove(s_file) + return ... + if not os.path.exists(s_file): + session.clear() + return ... +``` + +也就是说: + +```text +Nexus 一键登录成功 +→ 宝塔生成 tmp_login session 文件 +→ BT-Task 每 10 分钟清理 session +→ task.py 硬编码超过 3600 秒删除 +→ 用户 1~2 小时后刷新 +→ tmp_login session 文件不存在 +→ 宝塔清空 session +→ 用户被踢出 +``` + +--- + +## 3. 总体修复方案 + +### 3.1 Nexus 侧改进 + +Nexus 后端做四类改进: + +1. **不缓存成功的一键登录 URL** + - 宝塔 `tmp_token` 是一次性链接。 + - 如果缓存成功 URL,用户再次拿到的可能是已消费 token。 + +2. **一键登录生成加 Redis 分布式锁** + - 同一台宝塔服务器同时只允许一个 Nexus worker 生成登录 URL。 + - 避免 10 个页面同时一键登录时互相覆盖临时登录 token。 + +3. **宝塔临时登录 TTL 固定为 86400 秒** + - `set_temp_login` 传入 24 小时过期时间。 + +4. **Nexus bootstrap/repair 自动修复宝塔 session 清理硬编码** + - 检查 `/www/server/panel/task.py`。 + - 把 `if f_time > 3600:` 改为 `if f_time > session_cleanup_timeout:`。 + - `session_cleanup_timeout` 从宝塔 `public.get_session_timeout()` 读取。 + - 修改前备份。 + - 修改前做 Python 语法校验。 + - 保留原文件权限。 + +### 3.2 目标机器热修复 + +已对 `42.193.182.190` 执行热修复: + +- 备份:`/www/server/panel/task.py.nexus-session-ttl.bak` +- 修改:`/www/server/panel/task.py` +- reload:`/etc/init.d/bt reload` +- 状态:`Bt-Panel` 与 `Bt-Task` 均 running + +--- + +## 4. 调用链图:Nexus 一键登录 API / 服务 / 数据库 / 宝塔调用链 + +```mermaid +flowchart TD + A["前端点击:宝塔一键登录"] --> B["POST /api/btpanel/servers/{server_id}/login-url"] + B --> C["server/api/btpanel.py:create_bt_login_url"] + C --> D["BtPanelService.create_login_url"] + D --> E["Redis 分布式锁 login_url_lock(server_id)"] + E --> F["_create_login_url_fresh"] + F --> G{"bt_panel credentials 是否已配置"} + G -- "否" --> H["bootstrap_server(force=True)"] + H --> I["ssh_bootstrap_panel"] + I --> J["远程修改宝塔 api.json / 白名单 / session cleanup TTL"] + J --> K["更新 Server.extra_attrs.bt_panel"] + G -- "是" --> L["read_bt_panel_credentials"] + K --> L + L --> M["BtPanelClient.post /config?action=set_temp_login"] + M --> N["宝塔 default.db.temp_login 写入临时登录记录"] + N --> O["resolve_temp_login_url / normalize_temp_login_url"] + O --> P["写 AuditLog: bt_panel_login_url"] + P --> Q["返回 login URL 给前端"] +``` + +--- + +## 5. 本地 Nexus 代码改进清单 + +### 5.1 新增 Redis 分布式锁 + +文件: + +```text +C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\infrastructure\btpanel\login_url_lock.py +``` + +完整新增代码: + +```python +"""Distributed per-server lock for BT Panel one-click login URL generation. + +BT Panel temporary login tokens are effectively single-use and creating a new token can +invalidate a previous unused token. A process-local ``asyncio.Lock`` is not sufficient +when Nexus runs with multiple workers/containers, so this lock is backed by Redis. +""" + +from __future__ import annotations + +import asyncio +import logging +import secrets +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from server.infrastructure.redis.client import get_redis + +logger = logging.getLogger("nexus.btpanel.login_url_lock") + +_LOCK_KEY_PREFIX = "btpanel:login_url:lock" +_LOCK_TTL_SECONDS = 600 +_LOCK_WAIT_TIMEOUT_SECONDS = 120.0 +_LOCK_POLL_SECONDS = 0.2 + +_RELEASE_SCRIPT = """ +if redis.call("get", KEYS[1]) == ARGV[1] then + return redis.call("del", KEYS[1]) +end +return 0 +""" + + +class LoginUrlLockTimeout(RuntimeError): + """Raised when another worker is already generating a login URL too long.""" + + +def _lock_key(server_id: int) -> str: + return f"{_LOCK_KEY_PREFIX}:{server_id}" + + +@asynccontextmanager +async def login_url_lock(server_id: int) -> AsyncIterator[None]: + """Serialize login URL generation across all Nexus workers. + + Redis is mandatory in Nexus, so lock backend errors are treated as hard failures + rather than silently falling back to an unsafe process-local lock. + """ + + redis = get_redis() + key = _lock_key(server_id) + token = secrets.token_urlsafe(24) + deadline = asyncio.get_running_loop().time() + _LOCK_WAIT_TIMEOUT_SECONDS + while True: + acquired = bool(await redis.set(key, token, ex=_LOCK_TTL_SECONDS, nx=True)) + if acquired: + break + if asyncio.get_running_loop().time() >= deadline: + raise LoginUrlLockTimeout("BT Panel login URL is already being generated; please retry shortly") + await asyncio.sleep(_LOCK_POLL_SECONDS) + + try: + yield + finally: + try: + await redis.eval(_RELEASE_SCRIPT, 1, key, token) + except Exception: # noqa: BLE001 - lock expires automatically; log release failures for ops visibility + logger.warning("failed to release bt panel login url lock server=%s", server_id, exc_info=True) +``` + +设计点: + +- 锁 key:`btpanel:login_url:lock:{server_id}`。 +- 锁 TTL:600 秒,避免 worker 崩溃后死锁。 +- 等待超时:120 秒。 +- Lua 释放锁,避免误删其它请求持有的新锁。 + +--- + +### 5.2 登录 URL 不再缓存,始终 fresh 生成 + +文件: + +```text +C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\application\services\btpanel_service.py +``` + +核心代码: + +```python +async def create_login_url( + self, + server_id: int, + *, + admin_username: str, + ip_address: str | None, +) -> dict[str, str | bool]: + # BT Panel tmp_token is a one-time login link. Do not cache successful + # URLs: a repeated request after the user opens the link could receive + # an already-consumed token. Redis lock still prevents concurrent + # workers from creating tokens at the same time and invalidating each other. + try: + async with login_url_lock(server_id): + return await self._create_login_url_fresh( + server_id, + admin_username=admin_username, + ip_address=ip_address, + ) + except Exception as exc: + await self._audit_login_url_failure( + server_id, + admin_username=admin_username, + ip_address=ip_address, + error=exc.__class__.__name__, + ) + raise +``` + +关键变化: + +- 成功 URL 不缓存。 +- 所有一键登录请求进入 Redis 分布式锁。 +- 异常统一写 audit,便于排查。 + +--- + +### 5.3 临时登录 token TTL 固定为 24 小时 + +文件: + +```text +C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\infrastructure\btpanel\constants.py +``` + +代码: + +```python +"""Baota panel integration constants.""" + +# Temporary login token/session upper bound in seconds; passed to BT Panel set_temp_login expire_time. +BT_TEMP_LOGIN_TTL_SECONDS = 86400 + +# Login URL success results are intentionally not cached: BT Panel tmp_token is one-time. +# Concurrent generation is guarded by the Redis distributed lock in login_url_lock.py. +``` + +调用点: + +```python +expire_time = int(time.time()) + BT_TEMP_LOGIN_TTL_SECONDS +client = BtPanelClient(creds, server.id) +data = await client.post( + "/config?action=set_temp_login", + {"expire_time": expire_time}, +) +``` + +效果: + +- Nexus 调宝塔 `set_temp_login` 时传入 24 小时绝对过期时间。 +- 生产实测 `temp_login` 记录 TTL 约 86399 秒。 + +--- + +### 5.4 API 层把锁超时映射为 409 + +文件: + +```text +C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\api\btpanel.py +``` + +代码: + +```python +def _http_error(exc: Exception) -> HTTPException: + if isinstance(exc, ValueError): + return HTTPException(status_code=400, detail=str(exc)) + if isinstance(exc, BtPanelApiError): + return HTTPException(status_code=502, detail=str(exc)) + if isinstance(exc, LoginUrlLockTimeout): + return HTTPException(status_code=409, detail=str(exc)) + if isinstance(exc, RuntimeError): + return HTTPException(status_code=502, detail=str(exc)) + logger.exception("btpanel error") + return HTTPException(status_code=500, detail="宝塔操作失败") +``` + +设计点: + +- 同一服务器一键登录并发过多时,返回 `409 Conflict`。 +- 前端可以提示“正在生成登录链接,请稍后重试”。 +- 不返回 500,避免误判成系统异常。 + +--- + +### 5.5 Nexus bootstrap 自动修复宝塔 session 清理 TTL + +文件: + +```text +C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\infrastructure\btpanel\ssh_bootstrap.py +``` + +新增函数: + +```python +def ensure_session_cleanup_ttl(): + # Make Baota session cleanup honor session_timeout.pl instead of a hard-coded 1h. + task_path = os.path.join(PANEL_ROOT, "task.py") + if not os.path.isfile(task_path): + return False + with open(task_path, "r", encoding="utf-8") as f: + source = f.read() + marker = "session_cleanup_timeout = int(public.get_session_timeout() or 86400)" + patched_condition = " if f_time > session_cleanup_timeout:\n" + if marker in source: + if patched_condition not in source: + raise RuntimeError("task.py session cleanup patch incomplete") + return False + anchor = ( + " s_time = time.time()\n" + " f_list = os.listdir(sess_path)\n" + " f_num = len(f_list)\n" + ) + replacement = ( + " s_time = time.time()\n" + " session_cleanup_timeout = 86400\n" + " try:\n" + " session_cleanup_timeout = int(public.get_session_timeout() or 86400)\n" + " except Exception:\n" + " pass\n" + " f_list = os.listdir(sess_path)\n" + " f_num = len(f_list)\n" + ) + hard_coded = " if f_time > 3600:\n" + anchor_index = source.find(anchor) + if anchor_index < 0: + raise RuntimeError("task.py session cleanup anchor not found") + patched = source[:anchor_index] + replacement + source[anchor_index + len(anchor):] + condition_index = patched.find(hard_coded, anchor_index + len(replacement)) + if condition_index < 0: + raise RuntimeError("task.py session cleanup threshold not found") + patched = patched[:condition_index] + patched_condition + patched[condition_index + len(hard_coded):] + compile(patched, task_path, "exec") + original_mode = os.stat(task_path).st_mode & 0o7777 + backup = task_path + ".nexus-session-ttl.bak" + if not os.path.exists(backup): + with open(backup, "w", encoding="utf-8") as f: + f.write(source) + os.chmod(backup, 0o600) + tmp = task_path + ".nexus.tmp" + with open(tmp, "w", encoding="utf-8") as f: + f.write(patched) + os.chmod(tmp, original_mode) + os.replace(tmp, task_path) + return True +``` + +接入 bootstrap 主流程: + +```python +actions = [] +warnings = [] +try: + if ensure_session_cleanup_ttl(): + actions.append("patched_session_cleanup_ttl") +except Exception as exc: + warnings.append(f"session_cleanup_ttl_patch_failed:{exc.__class__.__name__}") +``` + +返回结果新增 warnings: + +```python +emit({ + "ok": True, + "base_url": base, + "api_key": token, + "actions": actions, + "warnings": warnings, + "reloaded": bool(actions), + "verify_ssl": False, +}) +``` + +设计点: + +- **精确匹配**:只匹配 `sess_expire` 中 `s_time/f_list/f_num` 附近的固定片段。 +- **幂等**:已经包含 `session_cleanup_timeout` 时不重复 patch。 +- **半补丁检测**:有 marker 但没有新判断时报警。 +- **备份**:首次修改前备份到 `task.py.nexus-session-ttl.bak`。 +- **语法校验**:写入前 `compile(patched, task_path, "exec")`。 +- **权限保留**:新文件保留原 `task.py` mode。 +- **不阻断 bootstrap**:patch 失败只进 `warnings`,不影响 API token/bootstrap 主流程。 + +--- + +## 6. 测试改进 + +文件: + +```text +C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\tests\test_btpanel_ssh_bootstrap.py +``` + +新增测试: + +```python +def test_remote_bootstrap_patches_session_cleanup_ttl(): + from server.infrastructure.btpanel import ssh_bootstrap as mod + + script = mod._REMOTE_SCRIPT + assert "patched_session_cleanup_ttl" in script + assert "session_cleanup_timeout = int(public.get_session_timeout() or 86400)" in script + assert "if f_time > session_cleanup_timeout" in script + assert "hard_coded = \" if f_time > 3600:\\n\"" in script + assert "compile(patched, task_path, \"exec\")" in script + assert "original_mode = os.stat(task_path).st_mode & 0o7777" in script + assert "os.chmod(tmp, original_mode)" in script + assert ".nexus-session-ttl.bak" in script +``` + +已做静态验证: + +```text +compile ok server\infrastructure\btpanel\ssh_bootstrap.py +remote script compile ok +compile ok tests\test_btpanel_ssh_bootstrap.py +``` + +完整 pytest 暂未跑完,原因见第 11 节。 + +--- + +## 7. 生产热修复:42.193.182.190 + +### 7.1 已执行动作 + +通过生产 Nexus API 走 SOCKS5: + +```text +SOCKS5: 127.0.0.1:10808 +Nexus API: https://api.synaglobal.vip +server_id: 395 +``` + +对目标机执行: + +1. 检查 `/www/server/panel/task.py` 当前逻辑。 +2. 备份为 `/www/server/panel/task.py.nexus-session-ttl.bak`。 +3. 修改 session 清理阈值。 +4. reload 宝塔。 +5. 回查修改结果和宝塔状态。 + +### 7.2 生产执行结果 + +执行结果: + +```text +exec_status HTTP/1.1 200 OK execution_id 5917 +status completed exit 0 +``` + +修复前: + +```text +998: if f_time > 3600: +``` + +修复后: + +```text +992: session_cleanup_timeout = 86400 +994: session_cleanup_timeout = int(public.get_session_timeout() or 86400) +1003: if f_time > session_cleanup_timeout: +``` + +备份: + +```text +backup 600 root:root 61810 2026-07-07 05:08:05 +0800 /www/server/panel/task.py.nexus-session-ttl.bak +``` + +宝塔状态: + +```text +Bt-Panel already running +Bt-Task already running +``` + +--- + +## 8. 生产远程修复脚本模板 + +> 注意:以下脚本不包含任何 Nexus 登录凭据。实际执行应通过 Nexus 脚本执行功能或 SSH,在目标机 root/sudo 环境运行。 + +```bash +set -e +P=/www/server/panel/pyenv/bin/python3 +[ -x "$P" ] || P=/www/server/panel/pyenv/bin/python +[ -x "$P" ] || P=python3 +RESULT_FILE=/tmp/nexus_bt_session_ttl_patch.result +rm -f "$RESULT_FILE" + +cat /www/server/panel/data/session_timeout.pl 2>/dev/null | sed 's/^/session_timeout=/' || true +sudo grep -n 'session_cleanup_timeout\|if f_time > 3600\|if f_time > session_cleanup_timeout' /www/server/panel/task.py || true + +sudo "$P" - <<'PY' +import os +import py_compile +import stat + +PANEL_ROOT = "/www/server/panel" +task_path = os.path.join(PANEL_ROOT, "task.py") +result_file = "/tmp/nexus_bt_session_ttl_patch.result" + +with open(task_path, "r", encoding="utf-8") as f: + source = f.read() + +marker = "session_cleanup_timeout = int(public.get_session_timeout() or 86400)" +patched_condition = " if f_time > session_cleanup_timeout:\n" + +if marker in source: + if patched_condition not in source: + raise SystemExit("patch_incomplete") + result = "already_patched" +else: + anchor = ( + " s_time = time.time()\n" + " f_list = os.listdir(sess_path)\n" + " f_num = len(f_list)\n" + ) + replacement = ( + " s_time = time.time()\n" + " session_cleanup_timeout = 86400\n" + " try:\n" + " session_cleanup_timeout = int(public.get_session_timeout() or 86400)\n" + " except Exception:\n" + " pass\n" + " f_list = os.listdir(sess_path)\n" + " f_num = len(f_list)\n" + ) + hard_coded = " if f_time > 3600:\n" + anchor_index = source.find(anchor) + if anchor_index < 0: + raise SystemExit("anchor_not_found") + patched = source[:anchor_index] + replacement + source[anchor_index + len(anchor):] + condition_index = patched.find(hard_coded, anchor_index + len(replacement)) + if condition_index < 0: + raise SystemExit("threshold_not_found") + patched = patched[:condition_index] + patched_condition + patched[condition_index + len(hard_coded):] + + backup = task_path + ".nexus-session-ttl.bak" + if not os.path.exists(backup): + with open(backup, "w", encoding="utf-8") as f: + f.write(source) + os.chmod(backup, 0o600) + + st = os.stat(task_path) + tmp = task_path + ".nexus.tmp" + with open(tmp, "w", encoding="utf-8") as f: + f.write(patched) + os.chmod(tmp, stat.S_IMODE(st.st_mode)) + py_compile.compile(tmp, doraise=True) + os.replace(tmp, task_path) + result = "patched" + +with open(result_file, "w", encoding="utf-8") as f: + f.write(result) +print(result) +PY + +RESULT=$(cat "$RESULT_FILE") +echo "patch_result=$RESULT" + +if [ "$RESULT" = "patched" ]; then + sudo /etc/init.d/bt reload || true +fi + +sudo grep -n 'session_cleanup_timeout\|if f_time > 3600\|if f_time > session_cleanup_timeout' /www/server/panel/task.py || true +sudo stat -c 'backup %a %U:%G %s %y %n' /www/server/panel/task.py.nexus-session-ttl.bak 2>/dev/null || true +sudo /etc/init.d/bt status 2>/dev/null || true +``` + +--- + +## 9. 部署方案 + +### 9.1 本地代码部署到 Nexus 生产 + +建议流程: + +1. 备份当前生产 Nexus 代码/镜像。 +2. 合入以下文件变更: + +```text +server/infrastructure/btpanel/login_url_lock.py +server/infrastructure/btpanel/constants.py +server/application/services/btpanel_service.py +server/api/btpanel.py +server/infrastructure/btpanel/ssh_bootstrap.py +tests/test_btpanel_ssh_bootstrap.py +``` + +3. 构建后端镜像。 +4. 发布到生产。 +5. 重启后端服务。 +6. 回归测试: + +```http +POST /api/btpanel/servers/{server_id}/login-url +``` + +7. 检查 audit: + +```text +action=bt_panel_login_url +method=api 或 ssh +lock=redis +``` + +8. 对一台测试机器触发 bootstrap/repair,确认返回: + +```json +{ + "actions": ["patched_session_cleanup_ttl"], + "warnings": [], + "reloaded": true +} +``` + +或者已修复时: + +```json +{ + "actions": [], + "warnings": [], + "reloaded": false +} +``` + +### 9.2 对已有机器批量修复 + +建议分批执行,不要一次修全部: + +1. 先选 1 台验证。 +2. 再选 5~10 台。 +3. 最后批量。 + +每台机器执行前检查: + +```bash +cat /www/server/panel/data/session_timeout.pl +sudo grep -n 'if f_time > 3600\|session_cleanup_timeout' /www/server/panel/task.py +``` + +执行后检查: + +```bash +sudo grep -n 'session_cleanup_timeout\|if f_time > session_cleanup_timeout' /www/server/panel/task.py +sudo stat /www/server/panel/task.py.nexus-session-ttl.bak +sudo /etc/init.d/bt status +``` + +--- + +## 10. 回滚方案 + +### 10.1 单机宝塔回滚 + +如果某台宝塔修复后异常,可以回滚: + +```bash +sudo cp -a /www/server/panel/task.py /www/server/panel/task.py.nexus-session-ttl.rollback-copy +sudo cp -a /www/server/panel/task.py.nexus-session-ttl.bak /www/server/panel/task.py +sudo /etc/init.d/bt reload +sudo /etc/init.d/bt status +``` + +回滚后会恢复宝塔原始 3600 秒 session 清理行为。 + +### 10.2 Nexus 代码回滚 + +回滚以下逻辑即可: + +- `ssh_bootstrap.py` 中 `ensure_session_cleanup_ttl()` 及调用。 +- `login_url_lock.py` 及 `create_login_url()` 内 Redis 锁使用。 +- `constants.py` 中 86400 TTL 如需恢复原值。 +- `btpanel.py` 对 `LoginUrlLockTimeout` 的 409 映射。 + +不建议回滚“不缓存 login URL”的逻辑,因为宝塔 `tmp_token` 是一次性链接,缓存成功 URL 本身有风险。 + +--- + +## 11. 依赖安装与测试现状 + +### 11.1 已创建虚拟环境 + +路径: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv +``` + +Python / pip: + +```text +pip 26.0.1 +python 3.14 +``` + +### 11.2 安装依赖尝试 + +用户要求安装: + +```text +pytest/sqlalchemy +``` + +项目依赖文件: + +```text +C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\requirements.txt +C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\requirements-dev.txt +``` + +尝试 SOCKS5 安装: + +```powershell +python -m pip install --proxy socks5://127.0.0.1:10808 -r requirements.txt -r requirements-dev.txt +``` + +失败原因: + +```text +ERROR: Could not install packages due to an OSError: Missing dependencies for SOCKS support. +``` + +含义:当前 venv 的 pip/requests 缺 `PySocks`,所以不能直接识别 `socks5://`。 + +尝试直连安装: + +```powershell +python -m pip install -r requirements.txt -r requirements-dev.txt +``` + +失败原因: + +```text +SSLError(SSLEOFError: UNEXPECTED_EOF_WHILE_READING) +No matching distribution found for fastapi==0.115.6 +``` + +含义:直连 PyPI SSL 被中断或网络不可用。 + +### 11.3 推荐安装方案 + +方案 A:如果本机有 HTTP 代理端口,优先用 HTTP 代理安装: + +```powershell +$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv" +$proj = "C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus" +& "$venv\Scripts\python.exe" -m pip install --proxy http://127.0.0.1:端口 -r "$proj\requirements.txt" -r "$proj\requirements-dev.txt" +``` + +方案 B:先离线/手动安装 PySocks,再走 SOCKS5: + +```powershell +$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv" +& "$venv\Scripts\python.exe" -m pip install PySocks +& "$venv\Scripts\python.exe" -m pip install --proxy socks5://127.0.0.1:10808 -r requirements.txt -r requirements-dev.txt +``` + +但 B 的第一步仍需要能联网,所以如果直连不可用,仍建议使用 HTTP 代理或镜像源。 + +方案 C:使用国内镜像源: + +```powershell +$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv" +$proj = "C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus" +& "$venv\Scripts\python.exe" -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r "$proj\requirements.txt" -r "$proj\requirements-dev.txt" +``` + +### 11.4 依赖安装成功后运行测试 + +```powershell +$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv" +$proj = "C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus" +cd $proj +$env:PYTHONDONTWRITEBYTECODE = "1" +& "$venv\Scripts\python.exe" -m pytest tests/test_btpanel_ssh_bootstrap.py tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_login_url.py -q +``` + +--- + +## 12. 代码审查结果 + +### 12.1 已解决风险 + +| 风险 | 原状态 | 改进后 | +|---|---|---| +| 宝塔 tmp_token 一次性 URL 被缓存 | 可能返回已消费 token | 不缓存成功 login URL | +| 多 worker 并发生成 token | 可能互相覆盖/失效 | Redis per-server lock | +| token TTL 太短 | 已确认不是短 TTL,但需固定 | 统一 86400 秒 | +| 宝塔 session 文件 3600 秒硬清 | 1~2 小时刷新掉线 | 跟随 session_timeout.pl | +| 宝塔 task.py 误改风险 | 直接替换可能误伤 | 精确 anchor + 语法校验 | +| 远程修复不可回滚 | 无备份风险 | `.nexus-session-ttl.bak` 备份 | +| 文件权限变化 | 可能破坏宝塔运行 | 保留原 task.py mode | +| bootstrap patch 失败影响主流程 | 可能导致 API 配置失败 | warning,不阻断主 bootstrap | + +### 12.2 剩余注意事项 + +1. **宝塔升级可能覆盖 `task.py`** + - 升级后需要重新触发 Nexus bootstrap/repair 或批量修复。 + +2. **当前不是永久登录** + - 当前按 `session_timeout.pl=86400`,约 24 小时。 + - 如果想更久,需要调大宝塔 `session_timeout.pl`,并接受安全风险。 + +3. **reload 宝塔可能让当前面板短暂重连** + - 单台修复时影响较小。 + - 批量修复建议低峰执行。 + +4. **Redis 必须可用** + - `login_url_lock` 不做不安全 fallback。 + - Redis 故障时一键登录会失败,避免并发隐患静默放大。 + +--- + +## 13. 验证清单 + +### 13.1 立即验证 + +对 `42.193.182.190`: + +```bash +sudo grep -n 'session_cleanup_timeout\|if f_time > session_cleanup_timeout' /www/server/panel/task.py +sudo stat /www/server/panel/task.py.nexus-session-ttl.bak +sudo /etc/init.d/bt status +``` + +期望: + +```text +session_cleanup_timeout = 86400 +session_cleanup_timeout = int(public.get_session_timeout() or 86400) +if f_time > session_cleanup_timeout +Bt-Panel running +Bt-Task running +``` + +### 13.2 业务验证 + +1. Nexus 一键登录 `42.193.182.190`。 +2. 同时打开 10 个宝塔页面。 +3. 等待 2 小时。 +4. 刷新页面。 +5. 期望:不再因为 3600 秒 session 清理导致掉线。 + +### 13.3 24 小时边界验证 + +如果需要验证 24 小时边界: + +- 24 小时内刷新应保持登录。 +- 超过 `session_timeout.pl` 后刷新可能要求重新登录,这是预期行为。 + +--- + +## 14. 最终结论 + +本次修复后,`42.193.182.190` 的“宝塔一键登录后 1~2 小时刷新掉线”问题已针对根因修复: + +```text +宝塔 task.py 不再硬编码 3600 秒清理 session,改为跟随 session_timeout.pl。 +``` + +Nexus 后端也已补上长期改进: + +```text +不缓存一次性登录 URL ++ Redis 分布式锁串行化同一服务器登录 URL 生成 ++ 24 小时 temp_login TTL ++ bootstrap 自动修复宝塔 session 清理 TTL ++ audit/warnings 可观测 +``` + +建议下一步: + +1. 安装 Python 测试依赖并跑相关 pytest。 +2. 把本地 Nexus 改动发布到生产。 +3. 对其它宝塔机器分批执行同类修复。 +4. 加一个周期性巡检:检测 `task.py` 是否被宝塔升级覆盖。 diff --git a/docs/release/20260708/Nexus-宝塔批量Session共性分析-20260707.md b/docs/release/20260708/Nexus-宝塔批量Session共性分析-20260707.md new file mode 100644 index 00000000..8a176aaf --- /dev/null +++ b/docs/release/20260708/Nexus-宝塔批量Session共性分析-20260707.md @@ -0,0 +1,91 @@ +# 宝塔批量 Session 巡检共性分析(2026-07-07) + +数据来源:`work/bt_bulk_session_audit.jsonl` + `work/bulk_servers_list.json`。本次为离线聚合分析,未重新 SSH 扫描服务器。 + +## 总体 + +在线巡检记录:420 台。 + +| 分类 | 数量 | +|---|---:| +| Alibaba Cloud Linux / OpenAnolis | 200 | +| Ubuntu | 155 | +| Debian | 65 | + +## hardcoded_3600 的 148 台 + +结论:不是 Ubuntu 为主,不全是 Ubuntu。 + +| OS | 数量 | +|---|---:| +| Alibaba Cloud Linux / OpenAnolis | 77 | +| Debian 12 bookworm | 43 | +| Ubuntu | 28 | + +更细: + +| 版本 | 数量 | +|---|---:| +| Alibaba Cloud Linux 4.0.3 | 49 | +| Debian GNU/Linux 12 bookworm | 43 | +| Ubuntu 24.04.4 LTS | 25 | +| Alibaba Cloud Linux 3.2104 U12.2 | 21 | +| Alibaba Cloud Linux 3.2104 U12.3 | 6 | +| Ubuntu 24.04.3 LTS | 3 | +| Alibaba Cloud Linux 3.2104 U12.1 | 1 | + +共性: + +- 148 台全部 `panel_ssl=1`。 +- 148 台全部 `check_client_hash=1`。 +- 148 台全部 `session_timeout=86400`,说明配置值存在。 +- 但 `task.py` 仍匹配到硬编码清理逻辑 `f_time > 3600`,所以属于“配置是 86400,但清理代码仍按 3600 秒删 session”的类型。 +- 主机名分布:阿里云风格 `iZ*` 114 台,腾讯云风格 `VM-*` 34 台。 + +## 未开启面板 SSL 的 112 台 + +结论:这批基本是 Ubuntu,但不是 100%。 + +| OS | 数量 | +|---|---:| +| Ubuntu | 104 | +| Alibaba Cloud Linux / OpenAnolis | 5 | +| Debian | 3 | + +更细: + +| 版本 | 数量 | +|---|---:| +| Ubuntu 24.04.4 LTS | 104 | +| Alibaba Cloud Linux 3.2104 U12.3 | 4 | +| Debian GNU/Linux 12 bookworm | 3 | +| Alibaba Cloud Linux 3.2104 U13 | 1 | + +共性: + +- 112 台全部 `panel_ssl=0`。 +- 111 台是 `unknown_task_pattern`,1 台 `no_panel`。 +- 105 台 `session_timeout=missing_default_86400`。 +- 105 台 `check_client_hash=0`。 +- 主机名分布:腾讯云风格 `VM-*` 98 台,阿里云风格 `iZ*` 7 台,其它自定义主机名 7 台。 + +## 关键关系 + +`hardcoded_3600` 和 `panel_ssl=0` 当前没有重叠: + +| 组合 | 数量 | +|---|---:| +| hardcoded_3600 且 panel_ssl=0 | 0 | +| hardcoded_3600 且 panel_ssl=1 | 148 | +| unknown_task_pattern 且 panel_ssl=0 | 111 | + +所以这不是同一类问题: + +1. `hardcoded_3600`:主要是宝塔 session 清理代码硬编码 3600 秒,且面板 SSL 已开启。 +2. `panel_ssl=0`:主要是 Ubuntu 24.04.4 + 腾讯云 VM-* 这一批,面板未开 SSL,且 task.py 代码模式与前一批不同,需要抽样后分类修。 + +## 处理建议 + +1. 先处理 148 台 `hardcoded_3600`:备份 `/www/server/panel/task.py` 后把 3600 秒硬编码改成读取 `public.get_session_timeout()`,这批模式清晰。 +2. 对 112 台 `panel_ssl=0` 不要套同一个 patch;先抽样 5~10 台看 `task.py` 与 `public.py/check_client_hash` 版本差异。 +3. `panel_ssl=0` 是否要统一开启 SSL,是策略问题,不建议默认自动改;需要单独定方案。 diff --git a/docs/release/20260708/Nexus-宝塔批量Session巡检-20260707.md b/docs/release/20260708/Nexus-宝塔批量Session巡检-20260707.md new file mode 100644 index 00000000..4f9930e1 --- /dev/null +++ b/docs/release/20260708/Nexus-宝塔批量Session巡检-20260707.md @@ -0,0 +1,138 @@ +# Nexus 宝塔批量 Session 巡检报告 - 2026-07-07 + +- 总服务器数:467 +- 在线目标数:420 +- 巡检结果数:420 + +## task.py session 清理状态统计 +- `error`: 2 +- `no_panel`: 1 +- `patched_timeout`: 149 +- `unknown_task_pattern`: 268 + +## 面板 SSL 状态统计 +- `panel_ssl=0`: 112 +- `panel_ssl=1`: 306 +- `panel_ssl=None`: 2 + +## 需要修复 `task.py 3600` 的机器:0 + +| server_id | domain | task_state | session_timeout | panel_ssl | +|---:|---|---|---|---:| + +## 未开启面板 SSL 的宝塔机器:112 + +| server_id | domain | task_state | session_timeout | panel_ssl | +|---:|---|---|---|---:| +| 508 | 119.29.98.54 | no_panel | missing_default_86400 | 0 | +| 509 | 43.139.95.214 | unknown_task_pattern | missing_default_86400 | 0 | +| 505 | 42.193.140.122 | unknown_task_pattern | missing_default_86400 | 0 | +| 506 | 111.230.27.20 | unknown_task_pattern | missing_default_86400 | 0 | +| 507 | 106.55.44.161 | unknown_task_pattern | missing_default_86400 | 0 | +| 503 | 111.230.71.100 | unknown_task_pattern | missing_default_86400 | 0 | +| 504 | 134.175.126.92 | unknown_task_pattern | missing_default_86400 | 0 | +| 510 | 159.75.154.203 | unknown_task_pattern | missing_default_86400 | 0 | +| 489 | 139.199.59.208 | unknown_task_pattern | missing_default_86400 | 0 | +| 490 | 119.91.117.54 | unknown_task_pattern | missing_default_86400 | 0 | +| 467 | 111.230.90.201 | unknown_task_pattern | missing_default_86400 | 0 | +| 468 | 119.29.80.171 | unknown_task_pattern | missing_default_86400 | 0 | +| 453 | 119.29.114.23 | unknown_task_pattern | missing_default_86400 | 0 | +| 454 | 175.178.40.17 | unknown_task_pattern | missing_default_86400 | 0 | +| 452 | 43.139.143.211 | unknown_task_pattern | missing_default_86400 | 0 | +| 449 | 1.12.47.125 | unknown_task_pattern | missing_default_86400 | 0 | +| 450 | 106.55.22.208 | unknown_task_pattern | missing_default_86400 | 0 | +| 451 | 134.175.227.87 | unknown_task_pattern | missing_default_86400 | 0 | +| 448 | 43.138.249.111 | unknown_task_pattern | missing_default_86400 | 0 | +| 447 | 122.152.235.93 | unknown_task_pattern | missing_default_86400 | 0 | +| 446 | 42.193.252.88 | unknown_task_pattern | missing_default_86400 | 0 | +| 445 | 134.175.67.244 | unknown_task_pattern | missing_default_86400 | 0 | +| 430 | 20.2.235.101 | unknown_task_pattern | missing_default_86400 | 0 | +| 443 | 42.193.214.33 | unknown_task_pattern | missing_default_86400 | 0 | +| 436 | 106.52.242.218 | unknown_task_pattern | missing_default_86400 | 0 | +| 431 | 193.112.70.74 | unknown_task_pattern | missing_default_86400 | 0 | +| 432 | 43.139.133.179 | unknown_task_pattern | missing_default_86400 | 0 | +| 435 | 139.199.72.167 | unknown_task_pattern | missing_default_86400 | 0 | +| 433 | 119.91.195.218 | unknown_task_pattern | missing_default_86400 | 0 | +| 429 | 43.138.248.173 | unknown_task_pattern | missing_default_86400 | 0 | +| 444 | 159.75.29.226 | unknown_task_pattern | missing_default_86400 | 0 | +| 428 | 123.207.4.3 | unknown_task_pattern | missing_default_86400 | 0 | +| 422 | 104.214.187.235 | unknown_task_pattern | missing_default_86400 | 0 | +| 434 | 42.194.204.225 | unknown_task_pattern | missing_default_86400 | 0 | +| 424 | 159.75.203.210 | unknown_task_pattern | missing_default_86400 | 0 | +| 426 | 42.194.226.62 | unknown_task_pattern | missing_default_86400 | 0 | +| 425 | 119.29.93.168 | unknown_task_pattern | missing_default_86400 | 0 | +| 427 | 106.52.179.21 | unknown_task_pattern | missing_default_86400 | 0 | +| 418 | 106.53.75.192 | unknown_task_pattern | missing_default_86400 | 0 | +| 417 | 119.91.139.218 | unknown_task_pattern | missing_default_86400 | 0 | +| 416 | 42.193.200.77 | unknown_task_pattern | missing_default_86400 | 0 | +| 414 | 42.193.186.43 | unknown_task_pattern | missing_default_86400 | 0 | +| 415 | 114.132.210.151 | unknown_task_pattern | missing_default_86400 | 0 | +| 406 | 20.189.126.177 | unknown_task_pattern | missing_default_86400 | 0 | +| 404 | 104.214.176.182 | unknown_task_pattern | missing_default_86400 | 0 | +| 403 | 40.81.22.70 | unknown_task_pattern | missing_default_86400 | 0 | +| 412 | 1.14.141.27 | unknown_task_pattern | missing_default_86400 | 0 | +| 411 | 119.29.18.166 | unknown_task_pattern | missing_default_86400 | 0 | +| 410 | 43.138.205.6 | unknown_task_pattern | missing_default_86400 | 0 | +| 413 | 159.75.108.190 | unknown_task_pattern | missing_default_86400 | 0 | +| 401 | 119.91.225.50 | unknown_task_pattern | missing_default_86400 | 0 | +| 400 | 106.53.183.89 | unknown_task_pattern | missing_default_86400 | 0 | +| 409 | 111.230.28.100 | unknown_task_pattern | missing_default_86400 | 0 | +| 398 | 114.132.95.88 | unknown_task_pattern | missing_default_86400 | 0 | +| 397 | 139.199.84.10 | unknown_task_pattern | missing_default_86400 | 0 | +| 395 | 42.193.182.190 | unknown_task_pattern | missing_default_86400 | 0 | +| 394 | 119.91.108.108 | unknown_task_pattern | missing_default_86400 | 0 | +| 399 | 139.199.172.228 | unknown_task_pattern | missing_default_86400 | 0 | +| 393 | 119.91.254.157 | unknown_task_pattern | missing_default_86400 | 0 | +| 388 | 139.199.19.233 | unknown_task_pattern | missing_default_86400 | 0 | +| 381 | 20.239.6.127 | unknown_task_pattern | missing_default_86400 | 0 | +| 389 | 106.55.92.145 | unknown_task_pattern | missing_default_86400 | 0 | +| 387 | 118.126.97.140 | unknown_task_pattern | missing_default_86400 | 0 | +| 385 | 42.193.180.53 | unknown_task_pattern | missing_default_86400 | 0 | +| 386 | 123.207.39.88 | unknown_task_pattern | missing_default_86400 | 0 | +| 396 | 175.178.110.156 | unknown_task_pattern | missing_default_86400 | 0 | +| 370 | 52.184.87.139 | unknown_task_pattern | missing_default_86400 | 0 | +| 352 | 118.195.145.210 | unknown_task_pattern | missing_default_86400 | 0 | +| 351 | 119.45.120.154 | unknown_task_pattern | missing_default_86400 | 0 | +| 346 | 119.91.56.90 | unknown_task_pattern | missing_default_86400 | 0 | +| 345 | 43.138.141.171 | unknown_task_pattern | missing_default_86400 | 0 | +| 343 | 43.139.22.128 | unknown_task_pattern | missing_default_86400 | 0 | +| 344 | 119.29.20.35 | unknown_task_pattern | missing_default_86400 | 0 | +| 342 | 124.220.63.74 | unknown_task_pattern | missing_default_86400 | 0 | +| 341 | 115.159.225.242 | unknown_task_pattern | missing_default_86400 | 0 | +| 336 | 119.91.193.11 | unknown_task_pattern | missing_default_86400 | 0 | +| 337 | 106.55.106.122 | unknown_task_pattern | missing_default_86400 | 0 | +| 338 | 193.112.168.105 | unknown_task_pattern | missing_default_86400 | 0 | +| 335 | 43.136.17.135 | unknown_task_pattern | missing_default_86400 | 0 | +| 306 | 139.199.186.231 | unknown_task_pattern | missing_default_86400 | 0 | +| 305 | 175.178.234.26 | unknown_task_pattern | missing_default_86400 | 0 | +| 304 | 129.204.131.206 | unknown_task_pattern | missing_default_86400 | 0 | +| 300 | 118.126.97.2 | unknown_task_pattern | missing_default_86400 | 0 | +| 296 | 1.12.55.146 | unknown_task_pattern | missing_default_86400 | 0 | +| 299 | 134.175.220.13 | unknown_task_pattern | missing_default_86400 | 0 | +| 295 | 175.178.3.33 | unknown_task_pattern | missing_default_86400 | 0 | +| 303 | 159.75.74.94 | unknown_task_pattern | missing_default_86400 | 0 | +| 279 | 42.193.183.168 | unknown_task_pattern | missing_default_86400 | 0 | +| 280 | 43.136.121.184 | unknown_task_pattern | missing_default_86400 | 0 | +| 273 | 159.75.14.33 | unknown_task_pattern | missing_default_86400 | 0 | +| 265 | 106.55.172.50 | unknown_task_pattern | missing_default_86400 | 0 | +| 260 | 106.52.124.36 | unknown_task_pattern | missing_default_86400 | 0 | +| 258 | 81.71.154.172 | unknown_task_pattern | missing_default_86400 | 0 | +| 259 | 134.175.237.241 | unknown_task_pattern | missing_default_86400 | 0 | +| 256 | 114.132.46.237 | unknown_task_pattern | missing_default_86400 | 0 | +| 255 | 119.91.37.28 | unknown_task_pattern | missing_default_86400 | 0 | +| 254 | 42.193.215.87 | unknown_task_pattern | missing_default_86400 | 0 | +| 257 | 43.136.98.34 | unknown_task_pattern | missing_default_86400 | 0 | +| 253 | 43.139.204.116 | unknown_task_pattern | missing_default_86400 | 0 | +| 246 | 43.136.118.134 | unknown_task_pattern | missing_default_86400 | 0 | +| 245 | 193.112.191.239 | unknown_task_pattern | missing_default_86400 | 0 | +| 218 | 118.31.51.101 | unknown_task_pattern | 86400 | 0 | +| 175 | 8.163.42.25 | unknown_task_pattern | 86400 | 0 | +| 168 | 39.108.112.104 | unknown_task_pattern | 86400 | 0 | +| 167 | 120.76.53.162 | unknown_task_pattern | 86400 | 0 | +| 147 | 119.29.250.198 | unknown_task_pattern | missing_default_86400 | 0 | +| 148 | 42.194.218.55 | unknown_task_pattern | missing_default_86400 | 0 | +| 136 | 119.91.140.22 | unknown_task_pattern | missing_default_86400 | 0 | +| 135 | 159.75.238.16 | unknown_task_pattern | missing_default_86400 | 0 | +| 123 | 47.106.77.23 | unknown_task_pattern | 86400 | 0 | +| 112 | 39.108.77.77 | unknown_task_pattern | 86400 | 0 | +| 117 | 8.163.108.1 | unknown_task_pattern | 86400 | 0 | \ No newline at end of file diff --git a/docs/release/20260708/Nexus-宝塔长期掉线修复与测试部署记录-20260707.md b/docs/release/20260708/Nexus-宝塔长期掉线修复与测试部署记录-20260707.md new file mode 100644 index 00000000..9ca16209 --- /dev/null +++ b/docs/release/20260708/Nexus-宝塔长期掉线修复与测试部署记录-20260707.md @@ -0,0 +1,425 @@ +# Nexus 宝塔长期掉线修复与测试部署记录(2026-07-07) + +## 1. 当前结论 + +本轮重点处理的是:宝塔面板“一键登录成功后,多个面板同时打开,约 1-2 小时后刷新陆续掉线”的问题。 + +当前状态: + +- Nexus 测试部署机:`192.168.124.219` +- Nexus 项目目录:`/opt/nexus-dev-current` +- Git 分支:`audit/security-review` +- 最新提交:`99edd30 chore: default compose ports for nexus test deploy` +- 宝塔长期会话修复提交:`a538358 fix: keep bt panel sessions alive after one-click login` +- Docker 服务已恢复: + - Nexus:`0.0.0.0:18600 -> 8600` + - MySQL:`0.0.0.0:13306 -> 3306` + - Redis:`0.0.0.0:16379 -> 6379` +- Health check:`http://127.0.0.1:18600/health` 返回 `ok` +- 容器内确认补丁已生效: + - `_SESSION_CLEANUP_TTL_CHECK_SECONDS = 86400` + - `_SESSION_CLEANUP_TTL_FAILED_RETRY_SECONDS = 3600` + +--- + +## 2. 用户现象与根因判断 + +用户反馈: + +> 不是登录失败,是登录宝塔以后几个小时后自动退出;一键登录同时打开 10 个宝塔,过 2 个小时陆续掉;刷新就掉线。 + +这个现象不像是 Nexus 生成登录 URL 失败,也不像 tmp_token 本身立即失效。更符合下面链路: + +1. Nexus 一键登录宝塔时生成临时登录 token。 +2. 用户成功进入宝塔面板。 +3. 之后是否持续在线,取决于宝塔面板内部 session 过期/清理逻辑。 +4. 如果宝塔面板仍然按老逻辑清理 session,例如 `task.py` 中固定 `if f_time > 3600:`,那么即使 Nexus 生成的 tmp_token TTL 是 86400 秒,登录后的 session 仍可能在约 1 小时左右被宝塔清理。 +5. 用户刷新页面时,宝塔发现 session 已被清理,于是表现为掉线。 + +因此,本轮修复重点不是“能不能生成一键登录 URL”,而是: + +- 确保已配置过宝塔 API 的服务器,也会在一键登录前做一次 session cleanup TTL 检查/修复。 +- 避免老服务器因为已经配置过 API 而跳过 bootstrap,导致宝塔 session 清理阈值仍停留在 3600 秒。 + +--- + +## 3. 本次代码改动摘要 + +### 3.1 `server/application/services/btpanel_service.py` + +新增/调整逻辑: + +- 新增检查周期: + - `_SESSION_CLEANUP_TTL_CHECK_SECONDS = 24 * 3600` + - `_SESSION_CLEANUP_TTL_FAILED_RETRY_SECONDS = 3600` +- 在 `create_login_url()` / `_create_login_url_fresh()` 的一键登录链路中,针对已经配置过宝塔 API 的服务器,也会 best-effort 触发 session cleanup TTL 检查。 +- 检查成功后 24 小时内不重复检查。 +- 检查失败后 1 小时内不反复重试,避免频繁 SSH。 +- 无 SSH 授权、缺中心 IP、SSH 失败时,不阻断用户登录,只记录状态。 + +### 3.2 `server/infrastructure/btpanel/ssh_bootstrap.py` + +增强宝塔 bootstrap 输出: + +- `session_cleanup_ttl_ok` +- `session_cleanup_ttl_patched` +- warnings 中记录 patch 失败或异常信息。 + +宝塔侧修复目标是将固定 3600 秒清理逻辑改成读取面板 session timeout: + +```python +session_cleanup_timeout = int(public.get_session_timeout() or 86400) +if f_time > session_cleanup_timeout: + ... +``` + +### 3.3 `server/infrastructure/btpanel/bootstrap_state.py` + +公开并记录 session cleanup 状态字段: + +- `bootstrap_last_warnings` +- `session_cleanup_ttl_ok` +- `session_cleanup_ttl_checked_at` +- `session_cleanup_ttl_last_error` +- `session_cleanup_ttl_patched` + +这些字段方便后续在后端/前端展示每台宝塔服务器是否已经做过长期 session 修复。 + +### 3.4 `tests/test_btpanel_login_url.py` + +新增测试: + +- 已配置宝塔 API 的服务器,一键登录前会执行一次 session cleanup TTL 检查。 +- 第二次一键登录不会在 24 小时窗口内重复执行。 +- 登录 URL 仍然是 fresh 生成,不缓存一次性 URL。 + +### 3.5 `docker-compose.yml` + +新增防复发修复: + +- 默认 MySQL 发布端口从 `3306` 改为 `13306`。 +- 默认 Redis 发布端口从 `6379` 改为 `16379`。 +- 默认 Nexus 发布端口从 `8600` 改为 `18600`。 +- 默认 CORS/API base 改到 `18600`。 +- Usage 注释改成推荐: + +```bash +docker compose --env-file docker/.env up -d --build +``` + +修复原因:Docker Compose 的 `env_file` 只会传给容器环境,不一定参与 compose 变量插值;如果忘记 `--env-file docker/.env`,端口会落回默认值。之前默认值是 `3306/6379/8600`,会和测试机上的宝塔 Redis 等服务冲突。 + +--- + +## 4. 宝塔一键登录调用链 + +```mermaid +flowchart TD + UI["前端:一键登录按钮"] --> API["POST /api/btpanel/servers/{server_id}/login-url"] + API --> SVC["BtPanelService.create_login_url"] + SVC --> LOCK["Redis login_url_lock(server_id)"] + LOCK --> FRESH["_create_login_url_fresh"] + FRESH --> CHECK["_ensure_session_cleanup_ttl_for_login"] + CHECK --> NEEDSSH{"需要/允许 SSH 检查?"} + NEEDSSH -- "否" --> APIURL["_try_login_url_via_api"] + NEEDSSH -- "是" --> SSHBOOT["ssh_bootstrap_panel"] + SSHBOOT --> PATCH["ensure_session_cleanup_ttl patch 宝塔 task.py"] + PATCH --> STATE["apply_bootstrap_success 写入 extra_attrs.bt_panel"] + STATE --> APIURL + APIURL --> BTAPI["宝塔 API: /config?action=set_temp_login"] + BTAPI --> TOKEN["生成 tmp_token, TTL 86400"] + TOKEN --> RESOLVE["resolve_temp_login_url"] + RESOLVE --> URL["返回登录 URL"] + APIURL -- "API 不可用" --> SSHLOGIN["ssh_temp_login_url fallback"] + SSHLOGIN --> URL +``` + +关键点: + +- Redis lock 防止同一台服务器并发点击互相覆盖 tmp_token。 +- tmp_token TTL 是 86400 秒。 +- 本次补上的是“登录后 session cleanup TTL”的检查,解决成功进入宝塔后过一段时间刷新掉线的问题。 + +--- + +## 5. 已验证测试 + +在测试机 `/opt/nexus-dev-current` 上通过: + +```bash +.venv/bin/python -m pytest tests/test_btpanel_login_url.py tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_ssh_bootstrap.py -q +``` + +结果: + +```text +25 passed +``` + +完整后端测试: + +```bash +.venv/bin/python -m pytest -q +``` + +结果: + +```text +728 passed, 1 skipped +``` + +运行容器验证: + +```text +Nexus health: ok +SESSION_CLEANUP_TTL_CHECK_SECONDS: 86400 +FAILED_RETRY_SECONDS: 3600 +``` + +--- + +## 6. 测试机 Docker 状态 + +当前容器状态: + +```text +nexus-nexus-1 Up healthy 0.0.0.0:18600->8600/tcp +nexus-mysql-1 Up healthy 0.0.0.0:13306->3306/tcp +nexus-redis-1 Up healthy 0.0.0.0:16379->6379/tcp +``` + +宿主机宝塔 Redis: + +```text +127.0.0.1:6379 +``` + +已避免 Nexus Redis 占用宿主机 `6379`。 + +--- + +## 7. Docker 镜像构建代理问题 + +本轮执行 `docker compose --env-file docker/.env up -d --build` 时,Docker build 阶段失败过一次: + +```text +proxyconnect tcp: dial tcp 127.0.0.1:10808: connect: connection refused +``` + +原因:测试机上的 Docker daemon 使用了 `127.0.0.1:10808` 代理;但这里的 `127.0.0.1` 指的是 Linux 测试机自己,不是 Windows 主机。如果 Linux 测试机本地没有运行代理服务,Docker 就无法通过这个地址拉镜像。 + +本轮恢复方式: + +- 使用测试机已有的本地镜像 `nexus-nexus:latest`。 +- 执行 `docker compose --env-file docker/.env up -d --no-build` 恢复服务。 +- 容器内确认代码已经包含本次宝塔长期会话修复。 + +后续建议二选一: + +1. 在 Linux 测试机本地启动可用代理,并确保 Docker daemon 能访问。 +2. 不使用 `127.0.0.1:10808`,改成测试机能访问的局域网代理地址,或者直接使用可用 Docker registry mirror。 + +--- + +## 8. 回滚方案 + +如需回滚本次宝塔长期会话修复: + +```bash +cd /opt/nexus-dev-current +git revert a538358 +``` + +如需只回滚 compose 默认端口防复发修复: + +```bash +cd /opt/nexus-dev-current +git revert 99edd30 +``` + +恢复部署推荐命令: + +```bash +cd /opt/nexus-dev-current +docker compose --env-file docker/.env up -d --no-build +``` + +如果镜像代理问题已解决,再使用: + +```bash +cd /opt/nexus-dev-current +docker compose --env-file docker/.env up -d --build +``` + +--- + +## 9. 仍需关注的边界问题 + +1. `_ensure_session_cleanup_ttl_for_login()` 复用了 `ssh_bootstrap_panel()`,会同时检查/修复宝塔 API token、IP 白名单和 session cleanup TTL。 + - 优点:能顺便修复 API 白名单漂移。 + - 风险:首次检查会增加一次 SSH 耗时。 + +2. `_server_has_ssh_auth()` 对 preset 的判断偏粗,只看配置里是否存在 preset id。 + - 后续安全巡检要确认 preset 缺失、失效、被删时是否降级得足够安全。 + +3. `apply_bootstrap_success()` 现在也可能把 session cleanup 维护检查计入 `bootstrap_attempts`。 + - 如果后续要精细统计,可以拆出单独的 maintenance check counter。 + +4. Docker build 代理配置需要单独治理。 + - 当前运行服务没问题,但未来如果要重新 build,需要先修 Docker daemon 代理或镜像源。 + +--- + +## 10. 后续安全巡检计划 + +### 阶段 1:宝塔登录链路复审 + +- [x] 一键登录 tmp_token TTL 检查 +- [x] Redis per-server lock 检查 +- [x] 已配置 API 老服务器跳过 bootstrap 的边界修复 +- [x] session cleanup TTL 检查/修复 +- [x] 单测与完整后端测试 +- [x] Docker 测试机运行验证 + +### 阶段 2:SSH / 命令执行风险巡检 + +重点文件: + +- `server/infrastructure/btpanel/ssh_bootstrap.py` +- `server/infrastructure/btpanel/ssh_login.py` +- `server/application/services/server_file_transfer_service.py` +- `server/infrastructure/ssh/*` + +检查项: + +- 是否存在 shell 注入风险。 +- 是否所有远程命令都有 timeout。 +- 是否所有用户输入进入命令前都做 quote/escape。 +- 是否有敏感信息进入日志。 +- 是否有 sudo / root 权限边界问题。 + +### 阶段 3:API 鉴权与权限边界巡检 + +重点: + +- `server/api/*.py` +- 依赖注入中的 `Depends` / 当前用户获取逻辑 +- API key / session / CSRF / CORS + +检查项: + +- 是否存在未鉴权管理接口。 +- 是否存在越权访问其他服务器资源。 +- 是否存在批量操作缺少权限校验。 +- CORS 是否过宽。 + +### 阶段 4:数据库与敏感字段巡检 + +重点: + +- `server/models/*` +- `server/repositories/*` +- `server/application/services/*` + +检查项: + +- 密码、token、key 是否加密存储。 +- 查询是否存在 SQL 注入风险。 +- extra_attrs 是否可能被污染或写入不可控结构。 +- 数据迁移是否兼容老数据。 + +### 阶段 5:Redis / 锁 / 并发巡检 + +重点: + +- 登录 URL 锁 +- bootstrap 锁 +- 批量 bootstrap 并发 +- 缓存失效策略 + +检查项: + +- 锁 TTL 是否足够。 +- 异常时锁是否释放。 +- 并发点击是否导致 token 覆盖。 +- 多 worker/多容器下是否一致。 + +--- + +## 11. 当前建议 + +短期: + +1. 用当前部署继续测试“一键登录同时打开 10 个宝塔,2 小时后刷新是否还掉线”。 +2. 优先测试之前最容易掉线的服务器,例如用户提到的 `42.193.182.190`。 +3. 如果仍掉线,下一步要查看该目标宝塔机器的 session cleanup patch 状态字段,而不是只看 Nexus 登录 URL 是否生成成功。 + +中期: + +1. 修 Docker daemon 代理,保证以后可稳定 `--build`。 +2. 把 session cleanup 状态展示到 Nexus 后台,方便看到每台宝塔是否已修复。 +3. 继续做 SSH/API/DB/Redis 安全巡检。 + +--- + +## 12. SSH/文件管理安全巡检阶段 1 补充(2026-07-07) + +用户已确认:`script_service` 是设计内“管理员远程执行命令”能力,天然允许任意 shell,不作为漏洞修复项。后续仅检查其鉴权、审计、敏感信息日志边界。 + +本轮继续修复非脚本服务的命令参数边界: + +- `112e617 fix: stop archive option injection via filenames` + - `remote_archive.py` 的 tar/zip 压缩命令增加 `--` 参数终止符。 +- `bcec78f fix: harden file manager shell operations` + - 文件管理 `rm/mv/mkdir/cp` 命令增加 `--` 参数终止符。 + - 递归 chmod/chown 禁止关键系统目录子树,例如 `/etc/nginx`、`/usr/*`、`/var/log/*`。 + - 仍允许常见 Web 根目录,例如 `/www/wwwroot/site`、`/var/www/html`。 + +验证: + +```text +pytest tests/test_file_permissions.py tests/test_schema_path_validators.py tests/test_remote_archive_commands.py -q +22 passed in 0.54s + +pytest -q +733 passed, 1 skipped in 11.77s +``` + +运行容器已热更新并重启,`http://127.0.0.1:18600/health` 返回 `ok`。 + +--- + +## 13. SSH/文件管理安全巡检阶段 2:安全解压(2026-07-07) + +新增提交: + +```text +845d7e2 fix: validate remote archives before extraction +``` + +修复内容: + +- `/api/sync/decompress` 不再直接执行 `tar xzf` / `unzip -o`。 +- 新增 `run_remote_decompress()`:先列归档成员,再校验,再解压。 +- 拒绝归档内部: + - 绝对路径,例如 `/etc/passwd` + - 路径穿越,例如 `../evil.txt` + - Windows 反斜杠路径,例如 `dir\evil.txt` + - 符号链接、硬链接、设备文件、FIFO、socket +- tar 解压增加: + +```bash +tar --no-same-owner --no-same-permissions -xzf -C +``` + +验证: + +```text +pytest tests/test_remote_archive_commands.py tests/test_schema_path_validators.py tests/test_file_permissions.py -q +31 passed in 0.55s + +pytest -q +742 passed, 1 skipped in 11.83s +``` + +运行容器已热更新并重启,`http://127.0.0.1:18600/health` 返回 `ok`。 diff --git a/docs/release/20260708/Nexus-新增宝塔Session自动修复-20260707.md b/docs/release/20260708/Nexus-新增宝塔Session自动修复-20260707.md new file mode 100644 index 00000000..49054f39 --- /dev/null +++ b/docs/release/20260708/Nexus-新增宝塔Session自动修复-20260707.md @@ -0,0 +1,123 @@ +# Nexus 新增宝塔 Session 自动检测/修复记录 + +日期:2026-07-07 +测试部署机:192.168.124.219 +项目目录:/opt/nexus-dev-current +分支:audit/security-review +提交:5ac3f56 fix: auto repair btpanel session cleanup ttl + +## 目标 + +1. 新增服务器/新增宝塔自动检测宝塔 `/www/server/panel/task.py` 是否存在 `if f_time > 3600:` 的 session 清理硬编码。 +2. 只在识别到精确可修复模式时自动备份并 patch。 +3. 一键登录前增加兜底检测/修复,避免已经配置过 API Key 的老服务器继续因为 1 小时清理而刷新掉线。 +4. 把检测/修复状态保存到 `servers.extra_attrs.bt_panel`,并在登录 URL 响应 diagnostics 里返回非敏感诊断信息。 + +## 修改文件 + +- `/opt/nexus-dev-current/server/infrastructure/btpanel/ssh_bootstrap.py` +- `/opt/nexus-dev-current/server/infrastructure/btpanel/bootstrap_state.py` +- `/opt/nexus-dev-current/server/application/services/btpanel_service.py` +- `/opt/nexus-dev-current/tests/test_btpanel_login_url.py` +- `/opt/nexus-dev-current/tests/test_btpanel_ssh_bootstrap.py` + +## 核心逻辑 + +### 新增/绑定宝塔时 + +`ssh_bootstrap_panel()` 远程执行 bootstrap 脚本时会: + +1. 检查 `/www/server/panel/task.py`。 +2. 如果已经包含 `session_cleanup_timeout = int(public.get_session_timeout() or 86400)` 且清理条件为 `if f_time > session_cleanup_timeout:`,记录为 `patched_timeout`。 +3. 如果命中旧模式: + - 备份到 `/www/server/panel/task.py.nexus-session-ttl.bak`; + - 将固定 3600 秒改成读取 `public.get_session_timeout()`; + - 保留原文件权限; + - 先执行 Python compile 和 `py_compile.compile(tmp, doraise=True)`; + - 原子替换 task.py; + - 重载宝塔 `/etc/init.d/bt reload`。 +4. 如果 task.py 结构不匹配,记录 `unknown_task_pattern`,不盲改。 + +### 一键登录前 + +`BtPanelService.create_login_url()` 在生成宝塔一键登录 URL 前会: + +1. 若服务器已配置宝塔 API,则调用 `_ensure_session_cleanup_ttl_for_login()`。 +2. 该检查按缓存节流: + - 成功后 24 小时内不重复修复; + - 失败后 1 小时后可重试。 +3. 修复失败不会阻断本次一键登录,只会记录 diagnostics 和状态。 + +## 新增状态字段 + +保存到:`servers.extra_attrs.bt_panel` + +- `session_cleanup_ttl_ok` +- `session_cleanup_ttl_patched` +- `session_cleanup_task_state` +- `session_cleanup_ttl_checked_at` +- `session_cleanup_ttl_last_error` +- `panel_ssl` + +`session_cleanup_task_state` 当前可能值: + +- `patched_timeout`:已经修好或本次修好; +- `unknown_task_pattern`:task.py 存在,但不是已知可安全 patch 的结构; +- `no_panel`:未找到 task.py; +- `error`:检查/修复异常。 + +## 安全边界 + +- 不保存、不输出宝塔 tmp_token。 +- 不显示 API Key 明文。 +- 不自动修改 `panel_ssl=0`。 +- 不自动改 `check_client_hash()`。 +- 不对未知 task.py 结构做模糊替换。 +- 修改 task.py 前一定备份,写入前做编译检查,写入使用原子替换。 + +## 验证 + +在测试部署机 `/opt/nexus-dev-current` 执行: + +```bash +.venv/bin/python -m py_compile \ + server/infrastructure/btpanel/ssh_bootstrap.py \ + server/infrastructure/btpanel/bootstrap_state.py \ + server/application/services/btpanel_service.py \ + tests/test_btpanel_login_url.py \ + tests/test_btpanel_ssh_bootstrap.py + +.venv/bin/python -m pytest \ + tests/test_btpanel_ssh_bootstrap.py \ + tests/test_btpanel_login_url.py \ + tests/test_btpanel_bootstrap_loop.py \ + tests/test_btpanel_login_url_route.py \ + tests/test_btpanel_ssh_login.py \ + tests/test_btpanel_temp_login_ttl.py -q +``` + +结果: + +```text +39 passed in 0.61s +``` + +测试容器已热更新并重启: + +```text +curl http://127.0.0.1:18600/health => ok +nexus-nexus-1 healthy +``` + +注意:测试机 `docker compose up -d --build nexus` 曾因为远端 docker 代理 `127.0.0.1:10808` 不可达导致 metadata 拉取失败,所以本次测试环境采用 `docker cp + docker restart` 热更新容器。代码已提交到工作树,后续可在代理可用时重建镜像。 + +## 后续发布正式环境建议 + +正式环境不要 `git reset`,建议继续采用之前方式: + +1. 备份正式容器内对应文件; +2. `docker cp` 本次 3 个后端文件到正式容器; +3. 容器内 `python -m py_compile`; +4. restart 容器; +5. `/health` 检查; +6. 抽一台新增/未修复宝塔做一键登录验证,确认 diagnostics 返回 `session_cleanup_task_state=patched_timeout`。 diff --git a/docs/release/20260708/Nexus-最新代码上下文索引与Zvec部署报告-20260707.md b/docs/release/20260708/Nexus-最新代码上下文索引与Zvec部署报告-20260707.md new file mode 100644 index 00000000..da6ba735 --- /dev/null +++ b/docs/release/20260708/Nexus-最新代码上下文索引与Zvec部署报告-20260707.md @@ -0,0 +1,246 @@ +# Nexus 最新代码上下文索引与 Zvec 部署报告 + +生成时间:2026-07-07 +测试机:`192.168.124.219` +本地工作区:`C:\Users\uzuma\Documents\Codex\2026-07-06\yuu` + +--- + +## 1. 已完成结论 + +已处理完成: + +1. 已从 Nexus 测试机同步最新代码快照,并重建代码上下文索引。 +2. 已清理上下文索引污染源:本地快照中误带入的 `.megamemory/knowledge.db` 已删除。 +3. 已确认本地上下文库没有 `.db/.sqlite/.sqlite3/.duckdb/.faiss` 文件残留。 +4. 已确认最新快照没有敏感命名文件纳入索引,例如 `.env`、`.pem`、`.key`、`secret`、`resolved`、`凭据` 等。 +5. 已增强索引脚本脱敏规则:排除 `.megamemory/vendor` 等目录,并对 PEM 私钥块做整体脱敏。 +6. 已在测试机安装 Zvec:独立安装在 `/opt/nexus-code-context/.venv`,不改 Nexus 主项目虚拟环境。 +7. 已建立 Zvec 本地代码向量库:`/opt/nexus-code-context/data/nexus_code_chunks.zvec`。 +8. 已导入清理后的脱敏 chunks:`1440` 条,导入失败数 `0`。 +9. 已验证 Zvec 向量检索可查到宝塔一键登录/session 相关代码。 +10. 没有使用 SQLite 作为新的上下文数据库;结构化索引仍是 JSONL/Markdown,向量库是 Zvec。 + +--- + +## 2. 最新上下文索引统计 + +来源快照: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 +``` + +统计: + +| 项 | 数量 | +|---|---:| +| 文件总数 | 2214 | +| 文本索引文件 | 2020 | +| Python/前端符号 | 2861 | +| API 路由 | 214 | +| Service 方法 | 324 | +| 调用边 | 16780 | +| 数据库调用点 | 437 | +| Redis key/调用点 | 729 | +| SSH/远程命令调用点 | 346 | +| 宝塔相关流 | 183 | +| 前端路由 | 31 | +| 脱敏代码块 | 1440 | +| 生成时间 | 2026-07-07 11:47:00 +0800 | + +本地索引目录: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context +``` + +主要产物: + +```text +work/nexus_code_context/context_summary.json +work/nexus_code_context/index/api_routes.jsonl +work/nexus_code_context/index/service_methods.jsonl +work/nexus_code_context/index/call_edges.jsonl +work/nexus_code_context/index/db_queries.jsonl +work/nexus_code_context/index/redis_keys.jsonl +work/nexus_code_context/index/ssh_commands.jsonl +work/nexus_code_context/index/btpanel_flows.jsonl +work/nexus_code_context/cache/chunks.redacted.jsonl +work/nexus_code_context/reports/*.md +work/nexus_code_context/scripts/build_context_index.py +work/nexus_code_context/scripts/query_context.py +work/nexus_code_context/scripts/import_chunks_to_zvec.py +work/nexus_code_context/scripts/query_zvec_context.py +``` + +--- + +## 3. SQLite / 污染处理结果 + +用户之前明确不喜欢 SQLite,也担心上下文库污染。处理结果: + +### 3.1 已发现并清理的问题 + +本地最新源码快照中曾误包含: + +```text +work/nexus_code_context/snapshots/.../.megamemory/knowledge.db +``` + +该文件属于项目外部记忆/缓存目录,不应进入 Nexus 代码上下文库。已删除,并在索引脚本中加入排除: + +```python +.megamemory +.cursor +.playwright-mcp +.skills +vendor +``` + +### 3.2 当前检查结果 + +当前本地上下文目录检查: + +```text +.db/.sqlite/.sqlite3/.duckdb/.faiss: 0 个 +敏感命名文件: 0 个 +PEM 私钥块命中: 0 个 +JWT 样式 token 命中: 0 个 +AWS AKIA key 命中: 0 个 +未脱敏 password/token/key 赋值命中: 0 个 +``` + +因此当前上下文库不再包含 SQLite 数据库,也没有发现上述明显 secret 形态污染。 + +--- + +## 4. Zvec 安装结果 + +Zvec 没有装进 Nexus 主项目 `.venv`,而是独立放在测试机: + +```text +/opt/nexus-code-context/.venv +``` + +版本: + +```text +zvec 0.5.1 +``` + +Zvec 数据目录: + +```text +/opt/nexus-code-context/data/nexus_code_chunks.zvec +``` + +Manifest: + +```json +{ + "collection": "/opt/nexus-code-context/data/nexus_code_chunks.zvec", + "chunks_file": "/opt/nexus-code-context/input/chunks.redacted.jsonl", + "embedding": "local_hashing_vector_no_external_api", + "dimension": 256, + "inserted_attempted": 1440, + "insert_status_non_ok": 0 +} +``` + +说明: + +- 没有用 Docker 部署 Zvec,所以不会占用宝塔/Nexus 端口。 +- 没有公网暴露 Zvec 服务。 +- 没有调用外部 embedding API 上传私有代码。 +- embedding 使用本地确定性 hashing vector,只用于代码上下文召回,不用于语义模型训练。 +- 原始输入是 `chunks.redacted.jsonl`,不是未脱敏源码。 + +--- + +## 5. Zvec 查询验证 + +已在测试机验证向量检索: + +查询: + +```bash +/opt/nexus-code-context/.venv/bin/python /opt/nexus-code-context/bin/query_zvec_context.py 'bt panel session cleanup keep alive one click login' --topk 5 --mode vector +``` + +返回命中包括: + +```text +server/infrastructure/btpanel/constants.py +server/infrastructure/btpanel/login_url_lock.py +server/application/services/btpanel_service.py +``` + +这说明当前 Zvec 代码上下文库可以召回宝塔一键登录/session 保活相关代码。 + +--- + +## 6. 后续使用命令 + +### 6.1 本地重建 JSONL/Markdown 索引 + +在本地工作区执行: + +```powershell +$env:PYTHONIOENCODING='utf-8' +python work/nexus_code_context/scripts/build_context_index.py +``` + +### 6.2 重新导入测试机 Zvec + +先把清理后的 chunks 和脚本同步到测试机,再执行: + +```bash +/opt/nexus-code-context/.venv/bin/python /opt/nexus-code-context/bin/import_chunks_to_zvec.py --chunks /opt/nexus-code-context/input/chunks.redacted.jsonl --collection /opt/nexus-code-context/data/nexus_code_chunks.zvec --recreate +``` + +### 6.3 查询 Zvec 上下文库 + +```bash +/opt/nexus-code-context/.venv/bin/python /opt/nexus-code-context/bin/query_zvec_context.py '你的问题关键词' --topk 8 --mode vector +``` + +示例: + +```bash +/opt/nexus-code-context/.venv/bin/python /opt/nexus-code-context/bin/query_zvec_context.py 'archive extract tar zip path traversal' --topk 8 --mode vector +``` + +--- + +## 7. 对 Nexus 代码审查的帮助 + +现在的上下文库可以辅助后续安全巡检: + +1. 通过 `api_routes.jsonl` 快速定位 API 入口。 +2. 通过 `service_methods.jsonl` 找业务服务边界。 +3. 通过 `call_edges.jsonl` 拼 API → Service → Repo/SSH/Redis 调用链。 +4. 通过 `db_queries.jsonl` 聚合 SQLAlchemy 查询和写入点。 +5. 通过 `ssh_commands.jsonl` 聚合远程 shell/SSH/SFTP/文件管理风险点。 +6. 通过 `btpanel_flows.jsonl` 专门追踪宝塔一键登录、session、token、保活修复相关代码。 +7. 通过 Zvec 对关键词/问题做代码块召回,先定位上下文,再做人工审查。 + +--- + +## 8. 当前限制 + +1. 当前 Zvec 检索优先使用本地 hashing vector。它稳定、离线、不上传代码,但语义能力弱于大模型 embedding。 +2. Zvec FTS 字段已按 schema 配置,但当前实际使用以 vector 模式为主;后续如果要做精确全文检索,可再单独调优 FTS index/tokenizer。 +3. 上下文库只用于辅助定位,不替代测试和人工安全审查。 + +--- + +## 9. 下一步建议 + +建议后续 Nexus 安全巡检按这个顺序推进: + +1. 先用 `api_routes.jsonl` + `service_methods.jsonl` 画全量 API/服务/数据库调用链。 +2. 对 `btpanel_flows.jsonl` 做二次审查,重点检查:token TTL、并发一键登录、Redis lock、审计日志、失败回退。 +3. 对 `ssh_commands.jsonl` 做逐项确认:鉴权、超时、输出截断、日志脱敏、危险 shell 参数边界。 +4. 对 `db_queries.jsonl` 检查多租户/权限过滤、分页限制、事务提交/回滚。 +5. 每次修复后重新跑索引和 Zvec 导入,确保上下文库跟代码同步。 diff --git a/docs/release/20260708/Nexus-正式环境遗漏补发记录-20260707.md b/docs/release/20260708/Nexus-正式环境遗漏补发记录-20260707.md new file mode 100644 index 00000000..26209bb6 --- /dev/null +++ b/docs/release/20260708/Nexus-正式环境遗漏补发记录-20260707.md @@ -0,0 +1,109 @@ +# Nexus 正式环境遗漏补发记录(2026-07-07) + +## 背景 + +用户反馈:正式页面里“全局 API Key / API Key”仍可见。排查后确认:此前正式环境只热更新了宝塔 diagnostics 相关后端文件,`524acd5 fix-stop-exposing-global-api-key-in-settings` 与 `fa44439 fix btpanel ssh temp login python env` 未完整补发到正式容器。 + +## 本次补发范围 + +来源:局域网测试机 `/opt/nexus-dev-current`。 + +补发到正式容器 `nexus-prod-nexus-1` 的文件: + +- `server/api/settings.py` + - GET `/api/settings/` 对敏感设置不再返回明文值。 + - `api_key` / `secret_key` / `encryption_key` / Telegram Bot Token 返回 `value=""` + `value_set=true/false`。 +- `server/infrastructure/btpanel/ssh_login.py` + - 补发 SSH 临时登录 Python 环境兼容修复。 +- `web/app/index.html` +- `web/app/assets/` + - 补发 SettingsPage 前端构建产物,使当前设置页不再展示旧的全局 API Key 明文字段。 + +## 补丁包 + +测试机生成: + +- `/tmp/nexus-settings-frontend-patch-20260707.tar.gz` +- SHA256:`a56153c16ca74a80597ffd134bed3aa2bcdcef1c77b7e67a0998bdd1b85a13d6` + +本地留存: + +- `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-settings-frontend-patch-20260707.tar.gz` + +正式机上传: + +- `/tmp/nexus-settings-frontend-patch-20260707.tar.gz` + +## 正式环境热更新结果 + +正式主机:`20.24.218.235` + +容器:`nexus-prod-nexus-1` + +执行动作: + +1. 校验补丁包 SHA256。 +2. 解压到 staging 目录。 +3. 备份容器旧文件。 +4. `docker cp` 覆盖后端与前端构建产物。 +5. `python -m py_compile` 检查两个 Python 文件语法。 +6. 重启容器。 +7. 访问 `/health` 健康检查。 + +结果: + +- 备份目录:`/tmp/nexus-settings-patch-backup-20260707-170246` +- staging 目录:`/tmp/nexus-settings-patch-20260707-170246` +- 健康检查:`ok` + +## 验证结果 + +### 1. API 不再返回全局 API Key 明文 + +通过正式域名 `https://api.synaglobal.vip` 登录后调用: + +- `GET /api/settings/` + +验证结果: + +- 登录:`HTTP/1.1 200 OK` +- Settings:`HTTP/1.1 200 OK` +- settings_count:`33` +- sensitive_keys_present:`api_key`, `secret_key`, `telegram_bot_token`, `telegram_offline_bot_token` +- sensitive_values_plaintext:`NO` +- `api_key_value_set`:`True` + +说明:正式 API 仍会告诉前端“已配置过 API Key”,但不会返回真实值。 + +### 2. 当前前端入口已更新 + +正式容器当前入口: + +- `/app/web/app/index.html` +- 引用入口 JS:`/app/assets/index-CASoZdpL.js` +- 当前 SettingsPage 构建文件:`SettingsPage-DFgw8KT9.js` + +检查当前 SettingsPage 构建文件未发现 `API` 文案输出;目录内旧的 `SettingsPage-*.js` 残留不代表正在被当前入口使用。 + +## 用户侧注意事项 + +如果浏览器还看到旧的“全局 API Key / API Key”区块,优先按以下顺序处理: + +1. 强制刷新页面(Windows:`Ctrl + F5`)。 +2. 退出 Nexus 后重新登录。 +3. 清除 `api.synaglobal.vip` 站点缓存。 +4. 确认访问的是正式域名 `https://api.synaglobal.vip`,不是本地 `http://localhost:18600/app/` 旧代理页面。 + +## 回滚方式 + +如需回滚本次热更新,可从正式机备份目录恢复: + +- `/tmp/nexus-settings-patch-backup-20260707-170246/settings.py` +- `/tmp/nexus-settings-patch-backup-20260707-170246/ssh_login.py` +- `/tmp/nexus-settings-patch-backup-20260707-170246/web-app` + +恢复后重启容器并检查 `/health`。 + +## 结论 + +本次已经把之前遗漏的正式环境文件补发完成。当前正式 API 不再泄露全局 API Key 明文;前端当前入口也已切到新的 SettingsPage 构建产物。 diff --git a/docs/release/20260708/Nexus-测试部署机环境修复与验证报告-20260707.md b/docs/release/20260708/Nexus-测试部署机环境修复与验证报告-20260707.md new file mode 100644 index 00000000..7a254534 --- /dev/null +++ b/docs/release/20260708/Nexus-测试部署机环境修复与验证报告-20260707.md @@ -0,0 +1,292 @@ +# Nexus 测试部署机环境修复与验证报告 + +生成时间:2026-07-07 06:55(Asia/Shanghai) + +## 1. 本轮结论 + +Nexus 局域网测试部署机 `192.168.124.219` 当前可作为测试部署机使用: + +- Docker 部署正常,Nexus / MySQL / Redis 三个容器均为 healthy。 +- Nexus 健康接口正常:`http://127.0.0.1:18600/health` 返回 `200 OK` / `ok`。 +- Web 入口正常:`http://192.168.124.219:18600/app/`。 +- 后端全量 pytest 已通过:`727 passed, 1 skipped in 11.75s`。 +- 前端 `type-check` 与 `build` 已通过。 +- 已确认机器已使用国内源,未执行 `linuxmirrors.cn` 换源脚本,避免重复改源带来风险。 + +## 2. 当前部署状态 + +部署目录: + +```text +/opt/nexus-dev-current -> /opt/nexus-dev-20260707-054832 +``` + +Docker 端口: + +| 服务 | 容器端口 | 宿主端口 | 状态 | +|---|---:|---:|---| +| Nexus | 8600 | 18600 | healthy | +| MySQL | 3306 | 13306 | healthy | +| Redis | 6379 | 16379 | healthy | + +测试机上同时存在宝塔服务,本轮没有修改宝塔文件,也没有重启宝塔: + +| 服务 | 端口 | +|---|---:| +| 宝塔面板 | 29938 | +| 宝塔 nginx | 80 / 888 | + +## 3. 国内源与代理状态 + +已检查到当前测试机已配置国内源: + +- Ubuntu apt:阿里云源。 +- Docker CE:腾讯云源。 +- pip:清华源。 +- npm:`https://registry.npmmirror.com`。 +- Docker registry mirror:`https://docker.1ms.run/`。 + +Docker daemon 代理已配置为: + +```text +HTTP_PROXY=socks5h://127.0.0.1:10808 +HTTPS_PROXY=socks5h://127.0.0.1:10808 +``` + +注意:这个 Docker 代理依赖本机到测试机的 SSH reverse tunnel;如果隧道断开,已存在镜像和容器不受影响,但后续拉新镜像可能失败。 + +## 4. 本轮已修复文件 + +所有上传到远端的文件均先在远端做了 `.bak-时间戳` 备份。 + +### 4.1 之前已上传的环境/测试修复 + +```text +/opt/nexus-dev-current/pytest.ini +/opt/nexus-dev-current/tests/conftest.py +/opt/nexus-dev-current/tests/test_agent_version.py +/opt/nexus-dev-current/tests/test_agent_heartbeat_stop.py +/opt/nexus-dev-current/tests/test_btpanel_login_url.py +/opt/nexus-dev-current/server/infrastructure/ssh/remote_probe.py +/opt/nexus-dev-current/tests/test_server_connectivity.py +``` + +主要修复点: + +1. 新增 `pytest.ini`,限制 pytest 只收集 `tests/`,避免误收集 `scripts/_browse_test.py`。 +2. `tests/conftest.py` 默认把普通测试设置为已安装模式,避免裸跑 pytest 时进入安装模式导致 API 返回 503。 +3. 测试环境缺少 `ENCRYPTION_KEY` 时注入固定测试 Fernet key,避免测试依赖真实部署 secrets。 +4. Agent 版本测试从旧的 `2.0.0` 更新为当前代码版本 `2.1.1`。 +5. 宝塔一键登录 URL 单测更新为“不缓存一次性 tmp_token URL”,符合近期长期掉线修复方向。 +6. `remote_probe.py` 对缺少 `psutil` 的目标机增加 `/proc` 降级采集,避免监控命令直接失败。 +7. `test_server_connectivity.py` 修复 mock 行字段数不足的问题。 + +### 4.2 本轮继续上传的链路测试修复 + +本轮新增上传: + +```text +/opt/nexus-dev-current/tests/chain/test_batch_job_flow.py +/opt/nexus-dev-current/tests/chain/test_script_exec_flow.py +``` + +远端备份时间戳: + +```text +20260707-064723 +``` + +修复原因: + +两个链路测试原来 monkeypatch 了全局 `asyncio.create_task`,导致 SQLAlchemy / aiosqlite 在测试 teardown 关闭连接时也拿到 `MagicMock`,最终报错: + +```text +TypeError: An asyncio.Future, a coroutine or an awaitable is required +RuntimeWarning: coroutine 'AsyncConnection.close' was never awaited +``` + +修复方式: + +- 不再污染全局 `asyncio` 模块。 +- 只替换业务模块里的 `asyncio` 引用。 +- 用 `_AsyncioProxy` 保留其他 `asyncio` 能力,只拦截业务代码创建后台任务的 `create_task()`。 + +示意: + +```python +class _AsyncioProxy: + def __getattr__(self, name): + return getattr(asyncio, name) + + def create_task(self, coro): + return capture_create_task(coro) + +monkeypatch.setattr( + "server.application.services.server_batch_service.asyncio", + _AsyncioProxy(), +) +``` + +另一个文件同理替换: + +```python +monkeypatch.setattr( + "server.application.services.script_service.asyncio", + _AsyncioProxy(), +) +``` + +## 5. 验证结果 + +### 5.1 本轮 targeted 测试 + +命令: + +```bash +cd /opt/nexus-dev-current +. .venv/bin/activate +python -m pytest -q \ + tests/chain/test_batch_job_flow.py::test_batch_category_job_completes_and_persists \ + tests/chain/test_script_exec_flow.py::test_execute_script_reaches_completed +``` + +结果: + +```text +2 passed in 0.97s +``` + +### 5.2 后端全量 pytest + +命令: + +```bash +cd /opt/nexus-dev-current +. .venv/bin/activate +python -m pytest -q +``` + +结果: + +```text +727 passed, 1 skipped in 11.75s +``` + +### 5.3 Docker / HTTP 健康检查 + +检查时间:2026-07-07 06:48:38 +08:00 + +```text +nexus-mysql-1 Up / healthy 0.0.0.0:13306->3306 +nexus-redis-1 Up / healthy 0.0.0.0:16379->6379 +nexus-nexus-1 Up / healthy 0.0.0.0:18600->8600 +``` + +健康接口: + +```text +HTTP/1.1 200 OK +ok +``` + +Web 入口: + +```text +HTTP/1.1 200 OK +/app/ +``` + +### 5.4 前端检查 + +命令: + +```bash +cd /opt/nexus-dev-current/frontend +npm run type-check -- --pretty false +npm run build +``` + +结果: + +```text +type-check 通过 +build 通过 +``` + +存在非阻断 warning: + +1. `:deep(...)` CSS 伪类 warning: + - 构建器提示 `deep` 不是有效 pseudo-class,建议 `::deep`。 + - 当前不阻断 build,但建议后续统一检查 Vue scoped CSS 写法。 +2. dynamic import warning: + - `src/router/index.ts` 同时被静态和动态导入,影响拆包效果,不影响功能。 +3. chunk size warning: + - Monaco / Vuetify 相关 bundle 较大,建议后续做按需加载和 chunk 拆分。 + +## 6. 宝塔长期掉线问题现状 + +用户描述的问题是: + +> 不是登录失败,而是一键登录同时打开多个宝塔后,过几个小时陆续掉线,刷新就掉。 + +当前 Nexus 代码方向已经覆盖了关键点: + +1. 每次一键登录 fresh 生成宝塔 tmp_token。 +2. 不缓存一次性登录 URL。 +3. Redis per-server lock 避免同一服务器并发生成 tmp_token 互相覆盖。 +4. temp login TTL 目标为 86400 秒。 + +但测试机宝塔本体此前检查到仍可能存在边界风险: + +```text +/www/server/panel/data/session_timeout.pl = 86400 +/www/server/panel/task.py 仍有 if f_time > 3600: +``` + +这意味着即使 session_timeout 设置为 86400,宝塔后台某些清理逻辑仍可能按 3600 秒判断,从而造成几个小时后刷新掉线。 + +本轮没有修改宝塔本体文件。后续如果要验证/修复,需要明确执行以下步骤: + +1. 备份 `/www/server/panel/task.py`。 +2. 只修改硬编码 session 清理阈值相关逻辑。 +3. 尽量不重启宝塔;如必须重启,先告知影响。 +4. 再做 2 小时以上多窗口一键登录保活测试。 + +## 7. 后续建议的代码安全巡检顺序 + +建议按下面顺序继续做 Nexus 安全巡检: + +1. **配置与 secrets** + - 检查 `.env`、Docker env、日志输出是否泄露 token/password/key。 + - 检查测试文件是否写入真实密钥。 +2. **认证与授权** + - JWT 登录、安装模式、白名单、管理员接口鉴权。 + - 一键登录宝塔的权限边界与审计日志。 +3. **宝塔一键登录链路** + - tmp_token 生成、并发锁、TTL、错误重试、URL 不缓存策略。 + - 多窗口/多服务器/刷新场景。 +4. **SSH 执行链路** + - 命令拼接、参数转义、超时、输出脱敏、失败回滚。 +5. **数据库访问** + - SQLAlchemy 查询是否参数化。 + - 事务边界、异步 session 生命周期。 +6. **Redis 缓存/锁** + - key 命名空间、TTL、锁释放、异常路径。 +7. **文件/脚本执行** + - 上传路径限制、脚本内容校验、目录穿越风险。 +8. **前端安全** + - XSS、URL 拼接、iframe/window.open、token 存储位置。 +9. **部署安全** + - Docker 端口暴露、默认密码、CORS、安全响应头、Nginx 代理配置。 +10. **回归测试补齐** + - 为本次发现的安装模式、一次性 URL、asyncio monkeypatch、psutil 降级补长期回归用例。 + +## 8. 当前是否还有环境问题 + +按本轮检查结果: + +- 测试机 Docker 部署没有发现阻断问题。 +- 后端 pytest 没有剩余失败。 +- 前端 type-check/build 没有失败。 +- 仍需关注的是前端 build warning 和宝塔本体 `task.py` 的 3600 秒清理逻辑。 + diff --git a/docs/release/20260708/Nexus-项目说明文档-20260708.md b/docs/release/20260708/Nexus-项目说明文档-20260708.md new file mode 100644 index 00000000..db400ea5 --- /dev/null +++ b/docs/release/20260708/Nexus-项目说明文档-20260708.md @@ -0,0 +1,314 @@ +# Nexus 项目说明文档(2026-07-08) + +## 1. 项目定位 + +Nexus 是一个服务器资产管理、远程运维、宝塔面板管理和自动化发布平台。当前重点能力包括: + +- 服务器资产管理; +- SSH / WebSSH / 文件管理; +- 批量脚本执行; +- 宝塔面板一键登录; +- 宝塔 Session TTL 自动检测与修复; +- 审计日志与安全巡检; +- 旧后台 `/app/` 与新后台 `/app-v2/` 双后台并行; +- 发布包制品化与 Gitea / Act Runner 自动发布方向。 + +## 2. 当前项目路径 + +源码快照目录: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 +``` + +发布文档目录: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs +``` + +最新发布包: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +SHA256: + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +## 3. 技术栈概览 + +### 后端 + +- Python / FastAPI 风格 API; +- SQLAlchemy; +- Redis; +- SSH / Paramiko 相关远程能力; +- Docker / 1Panel 部署脚本; +- 宝塔 API + SSH bootstrap 混合修复链路。 + +### 前端 + +旧后台: + +```text +web/app/ +``` + +新后台: + +```text +frontend-v2/ +web/app-v2/ +``` + +新后台技术栈: + +- React; +- TypeScript; +- Vite; +- Tailwind / shadcn 风格组件; +- TanStack Query; +- TanStack Table; +- Zustand。 + +### 部署/发布 + +- Docker / 1Panel; +- `deploy/apply-release-bundle.sh`; +- `deploy/preflight-release-host.sh`; +- `scripts/prepare_release_artifact.py`; +- `scripts/publish_release_bundle.py`。 + +## 4. 重要目录说明 + +```text +server/ 后端 API、服务、基础设施代码 +server/api/ FastAPI 路由层 +server/application/services/ 业务服务层 +server/infrastructure/ Redis、SSH、宝塔等基础设施封装 +frontend-v2/ 新后台源码 +web/app/ 旧后台静态产物 +web/app-v2/ 新后台静态产物 +deploy/ 部署、升级、回滚、1Panel 脚本 +scripts/ 本地 gate、打包、发布辅助脚本 +tests/ 后端和发布辅助脚本测试 +outputs/ 本轮审查、发布、说明文档 +``` + +## 5. 当前核心改动 + +### 5.1 宝塔长期不掉线修复 + +问题根因:部分宝塔版本 `task.py` 中硬编码 `3600` 秒清理 `tmp_login` session,导致 Nexus 一键登录后 1~2 小时刷新掉线。 + +当前修复方向: + +- 登录前自动检测/修复宝塔 hardcoded `3600`; +- 改为基于 `public.get_session_timeout()` 或默认 `86400`; +- 新增 Redis 分布式锁,避免同一宝塔并发签发 login URL 时 tmp_token 互相覆盖; +- SSH bootstrap 增加远端 flock / mktemp / atomic replace; +- 新增测试覆盖宝塔登录、TTL、bootstrap、并发锁等场景。 + +关键文件: + +```text +server/application/services/btpanel_service.py +server/infrastructure/btpanel/login_url_lock.py +server/infrastructure/btpanel/bootstrap_lock.py +server/infrastructure/btpanel/ssh_bootstrap.py +server/api/btpanel.py +``` + +### 5.2 `/app-v2` 第二后台 + +新增第二后台 `/app-v2/`,与旧后台 `/app/` 并行,方便逐步替换旧后台。 + +关键特性: + +- 更现代的 UI; +- 响应更快; +- 保留原功能入口; +- 敏感信息不展示; +- 全局 API Key 只显示“已配置/未配置”,不显示真实值; +- 前端构建产物有敏感扫描。 + +关键文件: + +```text +frontend-v2/ +web/app-v2/ +frontend-v2/scripts/scan-app-v2-sensitive.mjs +server/main.py +Dockerfile +Dockerfile.prod +``` + +### 5.3 发布制品化 + +当前发布不再只靠零散文件复制,而是生成完整 tar.gz 发布包。 + +关键脚本: + +```text +scripts/prepare_release_artifact.py 一键本地 gate + manifest + tar.gz + scan + metadata + validate-only +scripts/generate_release_bundle.py 生成干净发布包 +scripts/scan_release_bundle.py 扫描发布包是否含敏感/脏文件 +scripts/write_release_metadata.py 写 metadata 和 .sha256 +scripts/publish_release_bundle.py Python 版上传 + preflight + 可选 apply +scripts/check_text_integrity.py 检查 UTF-8 / 控制字符 / Markdown fence +``` + +服务器侧脚本: + +```text +deploy/preflight-release-host.sh 服务器发布前检查 +deploy/apply-release-bundle.sh 校验、备份、应用发布包、重建、验证 +deploy/sync_webapp_to_container.sh 同步 /app 与 /app-v2 静态资源到容器 +``` + +## 6. 当前验证状态 + +本地 release gate 已通过: + +- Python 编译检查; +- deploy shell `bash -n`; +- deploy excludes 检查; +- 文本完整性检查; +- `/app-v2 build:safe`; +- app-v2 敏感扫描; +- 宝塔专项测试 60 个; +- 发布辅助脚本测试 5 个。 + +合计 Python 测试: + +```text +65 passed +``` + +发布包验证: + +- tarball scan 通过; +- `apply-release-bundle.sh --validate-only` 通过; +- metadata / `.sha256` / tar 实际 SHA 一致; +- 不包含 `tmp/pytest-publish-release`; +- 不包含高置信真实敏感信息。 + +## 7. 发布方式 + +### 7.1 当前推荐:发布包方式 + +发布包: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +SHA256: + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +只上传并执行 preflight: + +```cmd +cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 +.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^ + --remote nexus ^ + --tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs +exus-release-20260708.tar.gz ^ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +确认 preflight 没问题后正式发布: + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^ + --remote nexus ^ + --tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs +exus-release-20260708.tar.gz ^ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^ + --apply +``` + +### 7.2 Gitea 发布方向 + +已有远端 Gitea 仓库: + +```text +http://66.154.115.8:3000/admin/Nexus.git +``` + +如果要用局域网机器 `192.168.124.219` 做新的代码托管平台,建议安装: + +```text +Gitea + Act Runner +``` + +含义: + +- Gitea:代码仓库、分支、Web 管理; +- Act Runner:执行 Gitea Actions / CI/CD / 自动发布。 + +推荐最终流程: + +```text +开发机 / Codex + -> git push 到 Gitea + -> Gitea Actions 触发 + -> Act Runner 跑 gate / 打包 + -> SSH 到正式服务器 + -> apply-release-bundle.sh 发布 + -> 验证 /health /app/ /app-v2/ +``` + +第一次建议先做手动触发或 release 分支触发,不要一 push 就直接发布生产。 + +## 8. 测试部署机说明 + +局域网测试部署机: + +```text +192.168.124.219 +``` + +已知用途: + +- Nexus 测试部署机; +- Web 入口曾记录为:`http://192.168.124.219:18600/app/`; +- 本地 Git 工作树:`/opt/nexus-dev-current`; +- 曾用于 pytest、代码上下文索引、Zvec 部署等。 + +当前记录显示:它有本地 Git 仓库/工作树,但没有明确证据表明已安装 Gitea 服务。 + +## 9. 安全注意事项 + +1. 不要提交 `.env`、私钥、真实 token、宝塔 key、数据库密码; +2. 发布包已排除 `.env`、`docker/.env.prod`、`web/data`、venv、node_modules、缓存等; +3. `/app-v2` 不显示全局 API Key; +4. 宝塔一键登录必须走后端接口,不在前端生成 tmp token; +5. `script_service` 是管理员远程执行命令功能,属于产品能力,不按普通漏洞处理; +6. Gitea Actions 如要生产发布,必须先配置好 Secrets 和最小权限 SSH Key。 + +## 10. 重要文档索引 + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-index-20260708.md +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-ready-checklist-20260708.md +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-manual-deploy-commands-20260708.md +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-python-publisher-20260708.md +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-publisher-tests-20260708.md +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-app-v2-双后台实施说明-20260707.md +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-API服务数据库调用链索引报告-20260707.md +``` + +## 11. 当前未完成事项 + +- 线上 `https://api.synaglobal.vip` 尚未由 Codex 亲自完成 SSH 发布验证; +- 原因是本线程仍使用 `auto_review` 审批,当前 CC Switch 不支持 `codex-auto-review`; +- 用户可手动执行发布命令,或改用 Gitea/Act Runner 发布。 + diff --git a/docs/release/20260708/PR-MERGE-main与release合并清单-20260709.md b/docs/release/20260708/PR-MERGE-main与release合并清单-20260709.md new file mode 100644 index 00000000..6285a3f2 --- /dev/null +++ b/docs/release/20260708/PR-MERGE-main与release合并清单-20260709.md @@ -0,0 +1,297 @@ +# Nexus 分支合并清单:`main` ↔ `release/20260708-axs` + +> **编写日期**:2026-07-09 +> **用途**:合并评审 / Gitea PR 描述 / 冲突解决对照表 +> **Gitea**:https://axs.kuma1xn.vip/admin/Nexus + +--- + +## 1. 两分支现状 + +| 分支 | 提交 | 说明 | +|------|------|------| +| **`main`** | `5520434d` | 主项目全量历史(598 提交);含运维侧 6~7 月功能 + **一键登录 bootstrap→SSH 回退** | +| **`release/20260708-axs`** | `74149e0d` | 发布快照(孤儿 squash);含 **宝塔 Session 1~2h 掉线修复** + **`/app-v2`** + 发布制品链 | + +两分支 **无共同祖先**,不能普通 fast-forward,需 `--allow-unrelated-histories` 或按模块 cherry-pick。 + +**推荐合并方向**:`release/20260708-axs` → `main`(保留 main 历史,吸收 release 独有能力)。 + +```bash +cd /path/to/Nexus +git fetch https://axs.kuma1xn.vip/admin/Nexus.git main release/20260708-axs +git checkout main +git merge FETCH_HEAD:release/20260708-axs -m "merge: release/20260708-axs 宝塔 Session + app-v2" --allow-unrelated-histories +``` + +--- + +## 2. `main` 已有、合并时需保留的功能 + +### 2.1 宝塔一键登录:bootstrap 失败恢复 SSH 回退(2026-07-08 · 运维侧) + +**问题**:2026-06-21「自动 bootstrap」后,新机 API 未就绪时 bootstrap 失败直接 `raise`,不再走 SSH `tools.py`,一键登录整链中断。 + +**行为**: + +- 未配置 API 时仍先 `source=immediate` bootstrap(best-effort) +- 非凭据类错误(`ssh_command_failed`、`bt_not_installed` 等)→ **继续 SSH 临时登录** +- 仅 `ssh_auth_failed` / `ssh_no_credentials` / `ssh_sudo_required` / `missing_center_ip` 硬中断 + +**关键代码**: + +| 文件 | 要点 | +|------|------| +| `server/application/services/btpanel_service.py` | `BOOTSTRAP_LOGIN_HARD_FAIL_ERRORS`、`_create_login_url_fresh` | +| `server/infrastructure/btpanel/ssh_bootstrap.py` | 检测 `class/common.py`(非空 panel 目录);写 `api.json` 前 `makedirs` config | +| `tests/test_btpanel_login_url.py` | `test_create_login_url_falls_back_to_ssh_when_bootstrap_fails` | + +**Changelog**:`docs/changelog/2026-07-08-btpanel-bootstrap-bt-installed-check.md` + +--- + +### 2.2 宝塔一键登录:基础能力(main 历史,release 亦有部分重叠) + +| 功能 | 文件 | +|------|------| +| 未配置时自动 bootstrap + 倒计时 | `useBtPanelLogin.ts`、`btpanel_service.py`(2026-06-21) | +| 临时 token 24h | `BT_TEMP_LOGIN_TTL_SECONDS`、`btpanel_service._login_url_via_api` | +| 登录 URL 短窗口 Redis 缓存 + 去重 | `login_url_lock.py`、`create_login_url` 双检锁 | +| 主列表/终端一键登录按钮 | `ServersPage.vue`、`TerminalPage.vue`、`ServerTableRowActions.vue` | +| 批量获取 API | `useBtPanelBatchBootstrap.ts` | +| API 端口漂移 SSH 修复 | `_try_login_url_via_api`、`_repair_ip_whitelist` | + +--- + +### 2.3 Agent 监测与告警 + +| 功能 | 文件 | Changelog | +|------|------|-----------| +| 告警连续超阈门控(默认 3 次才 Telegram) | `server/utils/alert_streak.py`、`alert_metrics.py`、`watch_alerts.py` | `2026-07-04-alert-streak-gate.md` | +| Agent CPU 双模式采样 | `web/agent/cpu_metrics.py`、`server/api/agent.py` | `2026-07-02-agent-cpu-sampling.md` | +| 监测槽双模式 / Pin | `watch_service.py`、`useWatchPins.ts` | `2026-07-01-agent-watch-dual-mode.md` | +| 离线巡检发报前 SSH 复检心跳 | `offline_daily_report_schedule.py` | `d50120d1` | +| 统计卡:未设路径 / 离线 | `useServerStatsCards.ts` | `2026-07-02-stats-unset-path-offline-card.md` | + +--- + +### 2.4 服务器列表与批量 + +| 功能 | 文件 | +|------|------| +| 分类筛选栏 / 备注弹窗 / 站点链接 | `ServerCategoryFilterBar.vue`、`ServerRemarkDialog.vue`、`serverSiteUrl.ts` | +| 未设路径表与主表对齐 | `ServerUnsetPathPanel.vue` | +| 检测路径:搜 `crmeb` 目录 | `server_batch_common.py` | `2026-07-05-detect-path-crmeb.md` | +| IP/域名定时检测 | `ip_domain_detect_schedule.py`、`ip_domain_detect_loop.py` | `2026-06-24-ip-domain-detect-schedule.md` | +| nginx 域名探测 | `nginx_domain_detect.py`、`site_host.py` | +| 跨服务器文件传输增强 | `server_file_transfer_service.py`、`useServerFileTransfer.ts` | +| 上传目录权限探测 | `files_upload_permissions.py` | + +--- + +### 2.5 后台清理与配置 + +| 功能 | 文件 | +|------|------| +| 告警日志 / 审计日志定期清理 | `alert_log_purge.py`、`audit_log_purge.py`、`alert_log_repo.py` | +| 订阅 IP 刷新主 IP 修复 | `ip_allowlist_refresh.py` | `2026-06-23-subscription-ip-refresh-primary-fix.md` | + +--- + +### 2.6 部署与发布源 + +| 功能 | 文件 | +|------|------| +| 默认 Gitea:`axs.kuma1xn.vip`(HTTPS) | `deploy/nexus-1panel.sh`、`nexus-1panel.secrets.sh.example` | +| 全量分批推送(避 Cloudflare 524) | `scripts/git-push-axs-full.sh` | +| 本机 rsync 生产部署 | `deploy/rsync-local-to-server.sh`、`deploy-production.sh` | + +--- + +## 3. `release/20260708-axs` 独有、合并时必须带入 `main` 的功能 + +> 详见 release 内 `docs/release/20260708/PR-MERGE-说明-20260709.md` + +### 3.1 宝塔 Session 1~2 小时掉线修复(核心) + +**现象**:多窗口一键登录宝塔,约 1~2h 刷新后陆续掉线。 + +**根因**:部分宝塔 `task.py` 硬编码 `3600` 秒清理 `tmp_login` session,与 Nexus 24h tmp_token 不一致。 + +**行为**: + +- SSH bootstrap / 一键登录前检测并 patch `task.py` → 读 `public.get_session_timeout()`,默认 `86400` +- 远端 `flock` + `mktemp` + atomic replace +- `extra_attrs.bt_panel` 记录 `session_cleanup_ttl_ok`、`session_cleanup_task_state` 等 +- 登录 API 响应带 `diagnostics`(TTL 修补状态) +- `bootstrap_lock` 带 `wait_timeout_seconds`,避免并发 bootstrap 互踢 + +**关键文件**: + +```text +server/application/services/btpanel_service.py # _ensure_session_cleanup_ttl_for_login 等 +server/infrastructure/btpanel/ssh_bootstrap.py # ensure_session_cleanup_ttl() 远端脚本 +server/infrastructure/btpanel/bootstrap_lock.py +server/api/btpanel.py # diagnostics 字段 +tests/test_btpanel_temp_login_ttl.py +tests/test_btpanel_bootstrap_lock.py +tests/test_btpanel_get_config.py +``` + +--- + +### 3.2 `/app-v2` 第二后台 + +| 项 | 说明 | +|----|------| +| 路径 | `/app-v2/`(旧 `/app/` 保留) | +| 技术栈 | React + TS + Vite + shadcn/ui + TanStack Query/Table + Zustand | +| 目录 | `frontend-v2/`、`web/app-v2/` | +| 构建 | `Dockerfile.prod` 多阶段、`frontend-v2/scripts/scan-app-v2-sensitive.mjs` | +| 路由 | `server/main.py` 挂载静态 | + +--- + +### 3.3 发布制品化工具链 + +```text +scripts/prepare_release_artifact.py +scripts/generate_release_bundle.py +scripts/scan_release_bundle.py +scripts/write_release_metadata.py +scripts/publish_release_bundle.py +scripts/run_local_release_gate.py +deploy/preflight-release-host.sh +deploy/apply-release-bundle.sh +``` + +发布包 SHA256(release 记录):`46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b` + +--- + +### 3.4 文档与 Skill(release 独有目录) + +```text +docs/release/20260708/ # 宝塔 Session、app-v2、安全巡检、发布记录 +.skills/nexus-btpanel-review/ +.skills/nexus-security-review/ +.skills/nexus-ssh-safety/ +``` + +--- + +## 4. 合并冲突热点(必须手工合) + +### 4.1 `server/application/services/btpanel_service.py` + +**两边都要保留**: + +| 来源 | 保留内容 | +|------|----------| +| **main** | `BOOTSTRAP_LOGIN_HARD_FAIL_ERRORS`;bootstrap 失败 → SSH 回退(非凭据类不 raise) | +| **release** | `_ensure_session_cleanup_ttl_for_login`;`_session_cleanup_ttl_*`;`diagnostics`;`BootstrapLockTimeout` | + +合并后 `_create_login_url_fresh` 推荐顺序: + +1. 未配置 → immediate bootstrap(失败按 main 规则决定是否继续) +2. 已配置 → `_ensure_session_cleanup_ttl_for_login` +3. API 临时登录 → SSH 回退 + +### 4.2 `server/infrastructure/btpanel/ssh_bootstrap.py` + +| 来源 | 保留 | +|------|------| +| **main** | `common.py` 真安装检测;`makedirs` config | +| **release** | `ensure_session_cleanup_ttl()`;`atomic_write_json`;`flock`;`session_cleanup_*` 字段 | + +### 4.3 `server/main.py` / `Dockerfile.prod` + +- 从 **release** 带入 `/app-v2` 静态挂载与构建阶段 +- 保留 **main** 现有中间件与安装向导逻辑 + +### 4.4 `tests/test_btpanel_login_url.py` + +- 合并 release 的 session TTL / lock 测试 +- 保留 main 的 `test_create_login_url_falls_back_to_ssh_when_bootstrap_fails` + +--- + +## 5. 合并后验证清单 + +```bash +# 单元测试(合并冲突解决后) +.venv/bin/pytest tests/test_btpanel_login_url.py tests/test_btpanel_ssh_bootstrap.py \ + tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_bootstrap_lock.py -q + +# 发布门禁(若合并了 release 脚本) +bash scripts/run_local_release_gate.sh +``` + +**人工验收**: + +- [ ] `/app/` 旧后台 200 +- [ ] `/app-v2/` 新后台 200(若合并 app-v2) +- [ ] 新机一键登录:bootstrap 失败仍能 SSH 打开(main 修复) +- [ ] 已装宝塔一键登录后 **2h 刷新仍在线**(release Session 修复) +- [ ] 同一子机连点一键登录不互踢 token(login_url_lock) +- [ ] `GET /api/btpanel/servers/{id}/login-url` 含 `diagnostics`(release) + +--- + +## 6. 合并后部署 + +```bash +# 生产(中心机) +cd /opt/nexus +git remote set-url origin https://axs.kuma1xn.vip/admin/Nexus.git +git fetch origin main && git checkout main && git reset --hard origin/main +sudo env NEXUS_ROOT=/opt/nexus bash deploy/nexus-1panel.sh upgrade --skip-git + +# 或 release 包路径 +bash deploy/preflight-release-host.sh +bash deploy/apply-release-bundle.sh +``` + +--- + +## 7. Gitea PR 摘要(可直接粘贴) + +```markdown +## 合并摘要 + +将 `release/20260708-axs` 合并入 `main`,统一 SSOT。 + +### release 带入 + +- 宝塔 task.py hardcoded 3600 → session_timeout 自动修复(解决 1~2h 掉线) +- `/app-v2` 第二后台 +- 发布制品化 / preflight / apply-release-bundle +- bootstrap_lock、login diagnostics + +### main 保留 + +- 一键登录 bootstrap 失败 → SSH 临时登录回退(新机 API 未就绪) +- ssh_bootstrap 真安装检测(common.py)+ config makedirs +- 6~7 月运维功能:告警连续门控、crmeb 路径检测、文件传输、日志清理等 +- axs Gitea 默认发布源 + 分批推送脚本 + +### 冲突重点 + +`btpanel_service.py`、`ssh_bootstrap.py` — 两套宝塔逻辑必须同时保留,见本文 §4。 + +### 验证 + +pytest 宝塔专项 + `/app` + `/app-v2` + 2h 刷新不掉线 +``` + +--- + +## 8. 相关文档索引 + +| 文档 | 路径 | +|------|------| +| release 原 PR 说明 | `docs/release/20260708/PR-MERGE-说明-20260709.md`(在 release 分支) | +| 宝塔掉线方案 | `docs/release/20260708/Nexus-宝塔一键登录长期掉线修复方案与代码改进.md` | +| app-v2 说明 | `docs/release/20260708/Nexus-app-v2-双后台实施说明-20260707.md` | +| main bootstrap 回退 | `docs/changelog/2026-07-08-btpanel-bootstrap-bt-installed-check.md` | +| Gitea 发布说明 | `docs/release/20260708/README-Gitea发布说明-20260708.md` | diff --git a/docs/release/20260708/PR-MERGE-说明-20260709.md b/docs/release/20260708/PR-MERGE-说明-20260709.md new file mode 100644 index 00000000..d6c99750 --- /dev/null +++ b/docs/release/20260708/PR-MERGE-说明-20260709.md @@ -0,0 +1,362 @@ +# Nexus PR 合并说明:宝塔 Session 稳定性修复与 /app-v2 双后台 + +> 目标分支:`release/20260708-axs` +> 本地提交基线:`1933f0af6e647818e716ed194cdb92d7ed7bb47a` +> 编写日期:2026-07-09 +> 推荐用途:复制本文件核心内容到 Gitea Pull Request 描述,用于合并评审。 + +## 1. 合并结论 + +建议合并。该分支主要解决 Nexus 的宝塔一键登录在 1~2 小时后刷新掉线的问题,同时加入 `/app-v2` 第二后台与发布制品化工具链。 + +本分支不是单点热修,而是一组围绕“宝塔长期登录稳定性 + 新后台并行上线 + 发布可验证”的完整改进: + +- 一键登录前自动检测并修复宝塔 `task.py` 中硬编码 `3600` 秒清理 `tmp_login` session 的问题; +- 对同一台宝塔服务器的一键登录生成过程加锁,避免并发打开多个宝塔时 tmp token / session 互相覆盖; +- 新增 `/app-v2` 第二后台,保持旧后台 `/app/` 可继续使用,支持渐进替换; +- 增强发布包、发布前检查、发布验证和回滚脚本; +- 补齐代码审查、调用链、安全巡检、发布记录等文档。 + +## 2. 背景问题 + +用户实际现象: + +```text +Nexus 一键登录宝塔成功 +同时打开多台/多窗口宝塔 +约 1~2 小时后刷新页面陆续掉线 +``` + +根因链路: + +```text +Nexus 一键登录成功 +→ 宝塔生成 tmp_login session 文件 +→ 宝塔 BT-Task 定时清理 session +→ 部分宝塔 task.py 硬编码超过 3600 秒删除 tmp_login session +→ 1~2 小时后刷新 +→ tmp_login session 已被删除 +→ 宝塔判定掉线 +``` + +此外,同时打开多个宝塔时,同一台宝塔如果被重复触发一键登录,可能出现登录 URL / tmp token 生成冲突,因此本分支加入了分布式锁与远端 bootstrap 锁。 + +## 3. 本次核心改动 + +### 3.1 宝塔一键登录长期不掉线修复 + +关键能力: + +- 登录前自动检测宝塔 `task.py` 是否存在 hardcoded `3600` 清理逻辑; +- 自动修复为读取 `public.get_session_timeout()`,无法读取时使用默认 `86400`; +- 对新增宝塔也执行兜底检测/修复; +- 一键登录前再次兜底检测/修复,避免漏修服务器; +- 修复过程使用 SSH bootstrap,包含远端 `flock`、`mktemp`、atomic replace,降低并发和半写入风险; +- 同一宝塔登录 URL 生成加 Redis 分布式锁,防止同一服务器被并发打开时 token/session 互相覆盖。 + +重点文件: + +```text +server/application/services/btpanel_service.py +server/infrastructure/btpanel/login_url_lock.py +server/infrastructure/btpanel/bootstrap_lock.py +server/infrastructure/btpanel/ssh_bootstrap.py +server/api/btpanel.py +``` + +相关测试: + +```text +tests/test_btpanel_bootstrap_lock.py +tests/test_btpanel_get_config.py +tests/test_btpanel_login_url.py +tests/test_btpanel_login_url_route.py +tests/test_btpanel_ssh_bootstrap.py +tests/test_btpanel_temp_login_ttl.py +``` + +### 3.2 `/app-v2` 第二后台 + +新增第二后台,路径为: + +```text +/app-v2/ +``` + +设计目标: + +- 不替换旧后台 `/app/`,双后台并行; +- 保留原功能入口,逐步迁移; +- 更现代、更快、更适合后续 UI 重构; +- 全局 API Key 等敏感值不直接展示真实值。 + +技术栈: + +```text +React + TypeScript + Vite +shadcn/ui + Tailwind +TanStack Query +TanStack Table +Zustand +``` + +重点文件: + +```text +frontend-v2/ +web/app-v2/ +frontend-v2/scripts/scan-app-v2-sensitive.mjs +server/main.py +Dockerfile +Dockerfile.prod +``` + +### 3.3 发布制品化与验证工具链 + +本分支加入/增强了发布包生成、扫描、元数据、发布前检查和发布辅助脚本: + +```text +scripts/prepare_release_artifact.py +scripts/generate_release_bundle.py +scripts/scan_release_bundle.py +scripts/write_release_metadata.py +scripts/publish_release_bundle.py +scripts/check_text_integrity.py +scripts/generate_release_diff.py +scripts/run_local_release_gate.py +``` + +服务器侧脚本: + +```text +deploy/preflight-release-host.sh +deploy/apply-release-bundle.sh +deploy/sync_webapp_to_container.sh +``` + +发布包: + +```text +C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz +``` + +SHA256: + +```text +46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +## 4. 验证结果 + +本地 release gate 已通过: + +```text +Python py_compile +bash -n deploy shell +部署排除规则检查 +文本完整性检查 +/app-v2 build:safe +app-v2 sensitive scan +宝塔专项 pytest +发布辅助脚本测试 +``` + +测试结果: + +```text +65 passed +``` + +发布包验证: + +```text +size_mb=27.02 +entries=2409 +OK: release bundle looks clean +apply-release-bundle.sh --validate-only passed +metadata / .sha256 / tar 实际 SHA 一致 +``` + +重点人工验证建议: + +- `/app/` 旧后台可访问; +- `/app-v2/` 新后台可访问; +- 宝塔一键登录可打开; +- 同一台宝塔连续/并发触发一键登录不会互相覆盖; +- 新增宝塔在一键登录前会自动检测/修复 hardcoded `3600`; +- 典型宝塔登录 2 小时后刷新仍保持登录状态。 + +## 5. 合并风险与边界 + +### 5.1 主要风险 + +1. **宝塔远端 patch 风险** + 修复逻辑会通过 SSH 修改目标宝塔服务器上的 `task.py`。本项目定位中,管理员远程执行命令属于产品能力,因此这不是普通越权漏洞,但合并后需要继续保持审计日志与失败可见性。 + +2. **不同宝塔版本兼容性** + 部分宝塔版本 `task.py` 内容可能不同。当前逻辑已经使用检测/备份/原子替换/失败回退策略,但建议继续抽检更多系统版本。 + +3. **双后台静态资源体积** + 分支包含旧后台和新后台静态资源,仓库和发布包会变大。当前发布包约 27MB,可接受。 + +4. **远端批量修复节奏** + 线上 420 台里曾识别出 148 台存在 hardcoded_3600。合并后建议继续采用分批检测/修复,不建议一次性对全部服务器无观察窗口地全量变更。 + +### 5.2 不属于本次合并要解决的问题 + +- 不在本次合并中重构所有后台 UI; +- 不强制删除旧后台 `/app/`; +- 不改变 `script_service` 管理员远程执行命令的产品定位; +- 不依赖账号密码替代宝塔 API 登录;账号密码一键登录可作为后续增强方案单独设计。 + +## 6. 推荐合并方式 + +### 6.1 推荐路径 + +推荐在 Gitea 上创建 Pull Request: + +```text +source: release/20260708-axs +target: main +``` + +PR 地址: + +```text +https://axs.kuma1xn.vip/admin/Nexus/pulls/new/release/20260708-axs +``` + +建议标题: + +```text +release: 宝塔 Session 长期登录修复与 /app-v2 双后台 +``` + +建议合并策略: + +```text +Create merge commit +``` + +理由:本分支是发布型分支,包含后端修复、前端新增、发布脚本和文档。保留 merge commit 更方便后续定位整次发布。 + +### 6.2 合并前检查 + +合并前建议在 Gitea 页面确认: + +- 目标分支是 `main`; +- source 分支是 `release/20260708-axs`; +- 文件变更中包含 `docs/release/20260708/` 说明; +- 没有 `.venv`、`node_modules`、`.env`、`.pem` 等运行环境文件; +- PR 描述中粘贴本说明的第 1~8 节。 + +## 7. 合并后部署方式 + +### 7.1 仅上传 + preflight + +```cmd +cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 + +.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^ + --remote nexus ^ + --tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b +``` + +### 7.2 正式 apply + +preflight 无异常后再加 `--apply`: + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^ + --remote nexus ^ + --tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^ + --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^ + --apply +``` + +### 7.3 部署后验证 + +```cmd +.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip +``` + +期望: + +```text +OK: health +OK: /app +OK: /app-v2 +OK: online release verification passed +``` + +## 8. 回滚策略 + +如果合并后未部署: + +```text +在 Gitea revert merge commit 即可。 +``` + +如果已经部署: + +1. 使用 `deploy/apply-release-bundle.sh` 生成的备份目录回滚; +2. 恢复上一版 `/opt/nexus` 文件; +3. 重启 Nexus 服务/容器; +4. 验证 `/health`、`/app/`; +5. 如果问题只影响 `/app-v2/`,可临时隐藏 `/app-v2/` 路由或静态资源,不影响旧后台 `/app/`。 + +宝塔远端 `task.py` 修复属于向更长 session timeout 的兼容性修复。若个别宝塔异常,优先按单台服务器恢复宝塔备份文件,不建议全局回滚。 + +## 9. 文档索引 + +本分支已包含详细文档,集中在: + +```text +docs/release/20260708/ +``` + +建议优先阅读: + +```text +docs/release/20260708/Nexus-项目说明文档-20260708.md +docs/release/20260708/Nexus-release-index-20260708.md +docs/release/20260708/Nexus-release-ready-checklist-20260708.md +docs/release/20260708/Nexus-宝塔一键登录长期掉线修复方案与代码改进.md +docs/release/20260708/Nexus-app-v2-双后台实施说明-20260707.md +docs/release/20260708/Nexus-manual-deploy-commands-20260708.md +``` + +## 10. 可直接复制到 Gitea PR 的摘要 + +```markdown +## 合并摘要 + +本 PR 修复 Nexus 宝塔一键登录 1~2 小时后刷新掉线的问题,并新增 /app-v2 第二后台与发布制品化工具链。 + +### 核心改动 + +- 一键登录前自动检测/修复宝塔 task.py hardcoded 3600 清理 tmp_login session 的问题; +- 基于 public.get_session_timeout() 或默认 86400 延长 tmp_login session 有效期; +- 新增 Redis 分布式锁,避免同一宝塔并发生成登录 URL/token 互相覆盖; +- SSH bootstrap 使用 flock / mktemp / atomic replace 降低远端 patch 风险; +- 新增 /app-v2 第二后台,旧后台 /app/ 保留; +- 增强发布包生成、扫描、preflight、validate-only、apply 与回滚脚本; +- 补齐发布说明、调用链、安全巡检、宝塔修复记录等文档。 + +### 验证 + +- release gate 通过; +- 宝塔专项测试通过; +- 发布辅助脚本测试通过; +- 合计 65 passed; +- 发布包扫描通过; +- apply-release-bundle.sh --validate-only 通过; +- 发布包 SHA256: 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b。 + +### 风险 + +- 会通过 SSH 对目标宝塔服务器 task.py 做自动修复,已加入检测、备份、锁和原子替换; +- /app-v2 与 /app 双后台并行,不删除旧后台; +- 建议合并后先 preflight,再 apply,并对宝塔一键登录做 2 小时刷新验证。 +``` diff --git a/docs/release/20260708/README-Gitea发布说明-20260708.md b/docs/release/20260708/README-Gitea发布说明-20260708.md new file mode 100644 index 00000000..a9f51e78 --- /dev/null +++ b/docs/release/20260708/README-Gitea发布说明-20260708.md @@ -0,0 +1,25 @@ +# Nexus Gitea 鍙戝竷璇存槑锛?026-07-08锛? + +鏈洰褰曟敹褰曟湰娆?Nexus 鍙戝竷銆佸鏌ャ€佸疂濉?Session 淇銆?app-v2 鍙屽悗鍙般€佺幆澧冧慨澶嶄笌鍙戝竷楠岃瘉鐩稿叧璇存槑鏂囨。銆? + +## 鏈鏍稿績鍐呭 + +- 瀹濆涓€閿櫥褰曢暱鏈熸帀绾夸慨澶嶏細鐧诲綍鍓嶈嚜鍔ㄦ娴?淇 hardcoded_3600锛岄伩鍏?tmp_login session 琚?1 灏忔椂纭竻銆? +- 鏂板瀹濆鑷姩鍏滃簳锛氭柊澧炴湇鍔″櫒鍜屼竴閿櫥褰曞墠閮戒細杩涜 session 淇妫€娴嬨€? +- /app-v2 绗簩鍚庡彴锛歊eact + TypeScript + Vite + shadcn/ui + Tailwind + TanStack Query/Table + Zustand銆? +- 鍙戝竷鍖呬笌鍙戝竷宸ュ叿閾撅細鏈湴 release gate銆乥undle scan銆乿alidate-only 宸查€氳繃銆? + +## 鎺ㄨ崘鍏ュ彛鏂囨。 + +- Nexus-椤圭洰璇存槑鏂囨。-20260708.md +- Nexus-release-index-20260708.md +- Nexus-release-ready-checklist-20260708.md +- Nexus-manual-deploy-commands-20260708.md +- Nexus-瀹濆涓€閿櫥褰曢暱鏈熸帀绾夸慨澶嶆柟妗堜笌浠g爜鏀硅繘.md +- Nexus-app-v2-鍙屽悗鍙板疄鏂借鏄?20260707.md + + +## 合并说明入口 + +- PR-MERGE-说明-20260709.md:用于 Gitea Pull Request 合并评审,可直接复制到 PR 描述。 +