Compare commits
527 Commits
d5c498549b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d3c3e5a74f | |||
| 92834559bc | |||
| 3938170a16 | |||
| 1c5b45732f | |||
| 00c9eddd19 | |||
| 6aaf292ad9 | |||
| a16ba169fd | |||
| 9b583ccc91 | |||
| 5520434d53 | |||
| bd041136c0 | |||
| 6859846b7c | |||
| d976cd03a3 | |||
| 41bff9dff6 | |||
| 65d8605b6c | |||
| d50120d1d2 | |||
| 0ec0aaf174 | |||
| 45d21026b7 | |||
| 71f0f53e6a | |||
| 113718b553 | |||
| da26d648a9 | |||
| d770b07609 | |||
| 73278667e0 | |||
| 76682e3be0 | |||
| 629d8b3e26 | |||
| 37720c4705 | |||
| 4f260f9257 | |||
| c8efbc2a90 | |||
| 2cb0b57281 | |||
| 80d6aad54b | |||
| 58843a1ef4 | |||
| 7add9384c1 | |||
| 6720ca3474 | |||
| 2e797f86b7 | |||
| 5ea5c3a411 | |||
| 3195787461 | |||
| 97c47a120f | |||
| 028dc2c8b6 | |||
| 51b6d0cdff | |||
| e5929694c3 | |||
| b768da3ab8 | |||
| 45ca57bf3f | |||
| 14bbfc4ab7 | |||
| 770469c4aa | |||
| b4436143a6 | |||
| 7c99298419 | |||
| 461ac81221 | |||
| 1c4c5af0e4 | |||
| d00294c86d | |||
| 82426b1941 | |||
| ebab836554 | |||
| ccc5419369 | |||
| d8ec4f1af4 | |||
| 9c9a51263c | |||
| 1a79c0fbfb | |||
| 86a45612c8 | |||
| 05b0fde197 | |||
| c91cb1cd9e | |||
| 3faa7a4f1c | |||
| b5419cd2c0 | |||
| 5c508ed14c | |||
| 825d0297c4 | |||
| 4bbc47faab | |||
| ba7c790f38 | |||
| adee25deb6 | |||
| 4acbf04322 | |||
| feb776616f | |||
| 72042598b0 | |||
| e95e522d4d | |||
| 40cba6a567 | |||
| 821cf12ade | |||
| deafd46a0e | |||
| dbb6bc4a55 | |||
| e833b923c8 | |||
| 2b0413c43e | |||
| a51a5c5341 | |||
| 80aaf855a3 | |||
| e42612e75f | |||
| 7a0001d8f4 | |||
| 3598d38b8e | |||
| d5ecb54d6e | |||
| ac98dc496c | |||
| 49b471eff9 | |||
| 647ebcb673 | |||
| 4c38bc05d3 | |||
| 907607e30b | |||
| bdbfef7110 | |||
| 8092000880 | |||
| e57d689fb2 | |||
| 75efd506f5 | |||
| 32a1885f0d | |||
| 7bb85aac41 | |||
| f37047f746 | |||
| 6b8965aaf6 | |||
| 6140d68875 | |||
| b46922fe92 | |||
| f6e8b29640 | |||
| e0133b9098 | |||
| 724375c884 | |||
| 989b17902b | |||
| 441aa0b9d2 | |||
| 540712500f | |||
| 17abd62261 | |||
| 3a4b2ff3eb | |||
| 2877fd90a4 | |||
| e6abd4ae73 | |||
| 29b053b3c0 | |||
| f11ad35708 | |||
| 84b388fae2 | |||
| 0a5111a959 | |||
| 31846c4212 | |||
| 2e8daad802 | |||
| a94d340ec0 | |||
| 8356425814 | |||
| 2a6f526bc9 | |||
| 996403ad86 | |||
| 4e117c04ea | |||
| 79105dcec8 | |||
| d3c0e57a08 | |||
| af912662de | |||
| c69e45a571 | |||
| 130cc840c7 | |||
| 704764bd7e | |||
| 426bafd169 | |||
| dc4593f8b5 | |||
| 341b130d9b | |||
| af47b24450 | |||
| aeca2fd7cb | |||
| 960598f504 | |||
| 6f998048b6 | |||
| 9b9a857720 | |||
| f127f07ab2 | |||
| dabfc128b3 | |||
| dd870859b5 | |||
| 700b3b4301 | |||
| cc34b9c3cc | |||
| 53b700f929 | |||
| fc4b46566e | |||
| 7209f53af9 | |||
| 283041fe2f | |||
| 85b0c3a980 | |||
| cbbcdae66d | |||
| c5e54da8de | |||
| a82d69c3e5 | |||
| fc837bfd43 | |||
| 7a9dc3d3fc | |||
| 1fefa4b6cb | |||
| 6a6f3f7614 | |||
| fe8c11b706 | |||
| b287707359 | |||
| 64c8bd74b8 | |||
| 1c0d7e9eb6 | |||
| a842af2405 | |||
| 81004cf0a5 | |||
| 3ba78cd10a | |||
| 213aad97a4 | |||
| c366ed5adc | |||
| 664efb9ed5 | |||
| adcd8010a5 | |||
| 863e49e518 | |||
| 56139a42fe | |||
| e3efbd4552 | |||
| 4d82ed4031 | |||
| b62f1039db | |||
| 9f78207aa2 | |||
| 3864d120fb | |||
| 0f6f91e61a | |||
| 091fb97291 | |||
| abeab9d3e4 | |||
| 972456a246 | |||
| 69068e2e39 | |||
| a401ea5a4d | |||
| 632891e6e5 | |||
| ff143c286d | |||
| 85ecd1f54f | |||
| 05c7aeb702 | |||
| d0526c06b7 | |||
| cb49fd9d6d | |||
| fe8b24ca0b | |||
| 6612c76dc6 | |||
| 86bc737f4c | |||
| 27da842a20 | |||
| a5b1d34cec | |||
| 1e83bb34bd | |||
| 8c86b50d09 | |||
| 8312a6cf81 | |||
| 1d1fb6285b | |||
| 835ef2c8ea | |||
| 09d4613e46 | |||
| 22d5d21c1f | |||
| 08a0157c95 | |||
| b8425cc059 | |||
| b220a5a762 | |||
| b011b60f9c | |||
| 442e005d27 | |||
| 908ae938b5 | |||
| b12fb4770b | |||
| 6150a39889 | |||
| 06af90d35e | |||
| 9723e0f528 | |||
| 118ec9f6b0 | |||
| 3d76c4a447 | |||
| 0b2d7ac50d | |||
| 3f995a74bb | |||
| df95a2429f | |||
| 5f63fb0a3f | |||
| c435413563 | |||
| c8b0663508 | |||
| fb6a4be797 | |||
| dfb0ea2d8f | |||
| f12a0b6a36 | |||
| 5ed3c28491 | |||
| 77cd9a5205 | |||
| 814262bd83 | |||
| 2b016f91af | |||
| 217c5d9935 | |||
| a225b655d9 | |||
| bdb3b3bc34 | |||
| e5a3f2524e | |||
| 9337f37da8 | |||
| 7ea94a60ab | |||
| 05006cb5df | |||
| 69087988b1 | |||
| a14fbb3df3 | |||
| c50f65e8ea | |||
| ea2507dbc0 | |||
| f60a5ea5bc | |||
| ca0de7d04d | |||
| 764703d401 | |||
| b8785d1d51 | |||
| 5b372fb8a2 | |||
| f9934bd4f1 | |||
| 66a4cba39b | |||
| 540a7fce05 | |||
| 2e911dcca7 | |||
| a5d5518054 | |||
| e3fe161ae5 | |||
| d0be2fdcf1 | |||
| 72d82d737b | |||
| eab35a35be | |||
| e5e2582743 | |||
| 722917bfc9 | |||
| ac17f91106 | |||
| 9ee5c8a708 | |||
| fb9dbcb11a | |||
| 72e46cef32 | |||
| e5d482264d | |||
| c9e08799f9 | |||
| 89865ec690 | |||
| ba69bc3355 | |||
| 49f159a8ce | |||
| 4e1347ba45 | |||
| 61b8d7d419 | |||
| 954bdbd972 | |||
| 314bece418 | |||
| 687e059a86 | |||
| baacdb0252 | |||
| 58bdf820dc | |||
| dd66d99f2d | |||
| 107b089b16 | |||
| 1f5daebeed | |||
| 4ba45abf38 | |||
| 5a56e5a8cb | |||
| 850038a3ec | |||
| beb802802a | |||
| 476d87c678 | |||
| 49f88a2cd8 | |||
| b11360015d | |||
| 3b2856f388 | |||
| 9d9fbc62f5 | |||
| 807f4c2448 | |||
| da061d35db | |||
| d0544c9bd2 | |||
| 3f5eacae60 | |||
| b25d0798f6 | |||
| 6b1d23c7d7 | |||
| 6951b8049f | |||
| da0424a268 | |||
| 569263dddb | |||
| 646929ddff | |||
| 0a7c5ee617 | |||
| 93400b7793 | |||
| a5aa5f81ad | |||
| 86c865e710 | |||
| 1bd69077a2 | |||
| f37ebf8621 | |||
| e42dc77127 | |||
| 536d520744 | |||
| 6276ea08b7 | |||
| c81fe3183f | |||
| 5e7c5beb43 | |||
| 9b7dba99ff | |||
| 222703d733 | |||
| d884ea00ca | |||
| 422dcae0ff | |||
| 343def7e77 | |||
| 83ce11f55c | |||
| e613aecc8e | |||
| f71da47ccf | |||
| 1acac6439a | |||
| feda614c52 | |||
| 6cef3d2942 | |||
| 00f44032ff | |||
| 8ddcf2ad0d | |||
| 65e4478b4b | |||
| 2f830424a4 | |||
| 7b33b745a0 | |||
| bfda70bbe7 | |||
| 2f67e9e2b9 | |||
| 811eb97fbf | |||
| 0100ab2582 | |||
| b866e944ab | |||
| a2ae74d582 | |||
| 9ac6a2d27f | |||
| 3cec226b89 | |||
| 51948469c2 | |||
| 4b5602a719 | |||
| 9c9c763e9c | |||
| 0cc1cc14a4 | |||
| f078d0e03b | |||
| 524dabcf7f | |||
| 0ccd1ba934 | |||
| a6673157a5 | |||
| 78798e3630 | |||
| 68fdcbae0a | |||
| 181560eca2 | |||
| c6f1d73ff5 | |||
| 6cb591212d | |||
| 7cbddf5d48 | |||
| cb738a8e9a | |||
| 5393252036 | |||
| 3bc2843642 | |||
| 1d6d26d612 | |||
| c65e3ea2a7 | |||
| 67dac0168f | |||
| 57d67c494b | |||
| 928e3c5fdb | |||
| 832e34fd98 | |||
| 19cfb7caaa | |||
| ea3046f635 | |||
| 39a8cf16cf | |||
| b62d4f6f9f | |||
| 44c19f329a | |||
| 9e924562d2 | |||
| e3eb0e65a5 | |||
| fc056059f9 | |||
| bd6467dff9 | |||
| e704b01093 | |||
| 246070e599 | |||
| d93c518a14 | |||
| 1cfac23c01 | |||
| 799140f6c4 | |||
| 8935081ac5 | |||
| ab431028c1 | |||
| cc2dad7f31 | |||
| 24b4439b2b | |||
| b8af1fc418 | |||
| b77a314b36 | |||
| d519113734 | |||
| 65c77155d8 | |||
| 2deb89b49c | |||
| b76295aaec | |||
| 8311821590 | |||
| 73fdcafb81 | |||
| 91d0d4104e | |||
| 7fa4bd6dff | |||
| a20cefcfbd | |||
| 6183047fd6 | |||
| df2c188f5d | |||
| 2d00d6a8eb | |||
| 080c1158d4 | |||
| facc1e8ae4 | |||
| 1ecd8e4542 | |||
| c6485168b6 | |||
| e9af02dd22 | |||
| c9baecfdfa | |||
| 3d5870b404 | |||
| afd32118b2 | |||
| d0308aaef6 | |||
| 04dda2c419 | |||
| e084d90c61 | |||
| 24aa8494e5 | |||
| 5c7775c10c | |||
| 7a6479dbfa | |||
| 0fdae981cf | |||
| 3419ab8a09 | |||
| d512460dc9 | |||
| 0f02e047cc | |||
| 35583115ff | |||
| f745714530 | |||
| 0d056a45e9 | |||
| 0ded4a28ec | |||
| 5ac4a5c7cf | |||
| 9c5ebdfdbd | |||
| c457cb4058 | |||
| 8b2263916c | |||
| 293a0d64fe | |||
| 94fbdf7b2d | |||
| e65c9993a2 | |||
| ae47eb42de | |||
| c570801119 | |||
| cba8b3d8f6 | |||
| caef4a216b | |||
| 507b957b4e | |||
| c70424a224 | |||
| e8c1acba9f | |||
| 34786737b4 | |||
| be633f3e58 | |||
| 00a3309005 | |||
| 3d15200cfa | |||
| d8aca5bf18 | |||
| cb60af4483 | |||
| 5a5adafb39 | |||
| c0296b9d8e | |||
| ca4aceba3e | |||
| b5dfa8a870 | |||
| 17cdc96bb0 | |||
| 6012af0b4d | |||
| 9bdf7e420f | |||
| 40d672fdbb | |||
| 557c8d28c1 | |||
| 9f7be66356 | |||
| ac38442383 | |||
| 14131f98f0 | |||
| 64503c0260 | |||
| d19f5af807 | |||
| 463841a740 | |||
| 36fde6e6ed | |||
| 1a61fae590 | |||
| a50f17941d | |||
| 362b28199f | |||
| 5213def150 | |||
| 5166660f3a | |||
| 48ef06babb | |||
| ede7fbaa64 | |||
| 4c4d41b8f5 | |||
| 32feb1b6db | |||
| ebe276ea29 | |||
| 84282e87ae | |||
| de8d860244 | |||
| 3f50a40d25 | |||
| dc48f71b25 | |||
| 913fd16bb9 | |||
| deffa8a8f4 | |||
| 64a8a18287 | |||
| cc0ca47d61 | |||
| 449373f24f | |||
| 730b665306 | |||
| f5bea6f0d2 | |||
| c808c28edf | |||
| d53309e37e | |||
| a8fe7fed0a | |||
| f91cd847a9 | |||
| 993a234152 | |||
| e709c72f1f | |||
| f7c4b95dd6 | |||
| 75864fe04f | |||
| ae22db4b3e | |||
| 0b82a3fee7 | |||
| d785c78d6d | |||
| 56906e8fe2 | |||
| da234c4e73 | |||
| 7193b88156 | |||
| ac66f5b4e7 | |||
| 6dd0b487b7 | |||
| fe211619aa | |||
| 0cbda8c840 | |||
| 5958c21048 | |||
| 02423cb803 | |||
| 152852e2c3 | |||
| 3c7036babf | |||
| 74de9d303e | |||
| ad2e7667d3 | |||
| 82c297776a | |||
| 77b332c037 | |||
| 97be1fd214 | |||
| 366bdc18fa | |||
| 280357d7d7 | |||
| fd9b7d85b5 | |||
| 8b82d6bab0 | |||
| 869f64ac18 | |||
| 69056491a7 | |||
| 02f1fa4ef3 | |||
| eadf7254a1 | |||
| 1c70e597a3 | |||
| 867e2aa552 | |||
| a2d392dae7 | |||
| 8b10f9d64a | |||
| b1aa235726 | |||
| 5afb18b745 | |||
| a1b0b2c514 | |||
| f591033309 | |||
| 89fa517693 | |||
| 973e23e038 | |||
| ad7f4d8d62 | |||
| fb4e51ec5a | |||
| 8cd091add8 | |||
| 6be989c299 | |||
| ccb78ed980 | |||
| 95aa8a610f | |||
| 9de6ac9904 | |||
| cf0725b037 | |||
| 3943ff4f3a | |||
| deb3a94ee6 | |||
| 99201f48fd | |||
| c72631a293 | |||
| a1276f38ad | |||
| 400dcae781 | |||
| cdd0be328a | |||
| fbf5eadcf2 | |||
| 9bba58a529 | |||
| 488838c989 | |||
| da43960f38 | |||
| 5fcc1e22a6 | |||
| 37ceaef6d9 | |||
| d414b945e1 | |||
| 4ca07fa252 | |||
| d2832567c7 | |||
| 14dc29f241 | |||
| b9e8db7a3f | |||
| 08d0baf762 | |||
| 9c063a788f | |||
| 5a42345bc0 | |||
| 6c5e6a2daa | |||
| ac0eb79707 | |||
| e37bc6cea9 | |||
| 2c6f3abbd0 | |||
| 8c8d2a74d5 |
@@ -1,69 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git checkout *)",
|
||||
"Bash(python -m py_compile server/database.py server/services/retry_engine.py server/services/sync_engine.py server/api/servers.py server/main.py)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(python -m py_compile server/api/servers.py)",
|
||||
"Bash(python -m py_compile agent/agent.py)",
|
||||
"Bash(python -m py_compile server/main.py server/api/servers.py)",
|
||||
"Bash(python -m py_compile server/main.py)",
|
||||
"Bash(python -m py_compile server/config.py server/api/servers.py server/services/retry_engine.py server/services/notify.py)",
|
||||
"Bash(python -m py_compile server/config.py)",
|
||||
"Bash(python -m py_compile server/database.py)",
|
||||
"Bash(python -m py_compile server/database.py server/services/scheduler.py server/api/servers.py server/main.py)",
|
||||
"Bash(python -m py_compile server/services/retry_engine.py)",
|
||||
"Bash(python -m py_compile server/config.py server/main.py server/api/servers.py)",
|
||||
"Bash(python -m py_compile agent/agent.py server/api/servers.py)",
|
||||
"Bash(python -m py_compile server/services/sync_engine.py server/services/retry_engine.py server/api/servers.py)",
|
||||
"Bash(xargs python *)",
|
||||
"Bash(echo \"EXIT:$?\")",
|
||||
"Bash(python -m py_compile server/services/sync_engine.py agent/agent.py)",
|
||||
"Bash(python -m py_compile server/api/auth.py server/services/retry_engine.py)",
|
||||
"Bash(python -m py_compile server/main.py server/database.py)",
|
||||
"Bash(python -m py_compile server/config.py server/main.py)",
|
||||
"Bash(python -m py_compile server/services/monitor.py server/main.py)",
|
||||
"Bash(python -m py_compile server/services/monitor.py)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"Bash(sshpass -p 'root' ssh-copy-id -o StrictHostKeyChecking=no root@192.168.124.10)",
|
||||
"Bash(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@192.168.124.10 \"hostname && uname -a\")",
|
||||
"Bash(apt-get install *)",
|
||||
"Bash(yum install *)",
|
||||
"Read(//c/Users/uzuma/.ssh/**)",
|
||||
"Bash(python -c ' *)",
|
||||
"Bash(ssh -o ConnectTimeout=5 root@192.168.124.10)",
|
||||
"Bash(awk '{print $2}')",
|
||||
"WebFetch(domain:47.76.187.108)",
|
||||
"WebFetch(domain:api.synaglobal.vip)",
|
||||
"Bash(pip install *)",
|
||||
"Bash(where ssh *)",
|
||||
"Bash(ssh -V)",
|
||||
"Bash(cp \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\.mcp.json\" \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.mcp.json\")",
|
||||
"Bash(cp \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\mcp\\\\mcp_bridge.py\" \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\mcp\\\\mcp_bridge.py\")",
|
||||
"Read(//c/Users/uzuma/Desktop/svn/multi-server-sync/**)",
|
||||
"Read(//c/Users/uzuma/.claude/**)",
|
||||
"WebSearch",
|
||||
"Bash(cmd /c *)",
|
||||
"Bash(echo \"EXIT: $?\")",
|
||||
"Bash(ssh -o StrictHostKeyChecking=no -i /c/Users/uzuma/.ssh/id_rsa_mcp root@47.76.187.108 \"python3 /www/wwwroot/api.synaglobal.vip/mcp/multisync_server.py\")",
|
||||
"Bash(scp -o StrictHostKeyChecking=no -i /c/Users/uzuma/.ssh/id_rsa_mcp \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\mcp\\\\multisync_server.py\" root@47.76.187.108:/www/wwwroot/api.synaglobal.vip/mcp/multisync_server.py)",
|
||||
"Bash(git push *)",
|
||||
"Bash(claude mcp *)",
|
||||
"Skill(update-config)",
|
||||
"Skill(update-config:*)",
|
||||
"Bash(python -c \"import json; d=json.load\\(open\\(r'C:\\\\Users\\\\uzuma\\\\.claude.json'\\)\\); print\\(json.dumps\\(d.get\\('mcpServers',{}\\), indent=2\\)\\)\")",
|
||||
"mcp__multisync__service",
|
||||
"mcp__multisync__api_call",
|
||||
"mcp__multisync__git_log",
|
||||
"mcp__multisync__deploy",
|
||||
"mcp__multisync__config_view",
|
||||
"mcp__multisync__db_query",
|
||||
"mcp__multisync__logs",
|
||||
"mcp__multisync__run_test",
|
||||
"Bash(python -m py_compile \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\server\\\\api\\\\servers.py\")",
|
||||
"Bash(python -m py_compile \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\agent\\\\agent.py\")",
|
||||
"Bash(python -m py_compile \"C:\\\\Users\\\\uzuma\\\\Desktop\\\\svn\\\\multi-server-sync\\\\.claude\\\\worktrees\\\\unruffled-shtern-bca88d\\\\server\\\\services\\\\agent_installer.py\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mysql-mcp": {
|
||||
"command": "bash",
|
||||
"args": [
|
||||
"scripts/linux_mcp_mysql.sh"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"gitea-mcp": {
|
||||
"command": "bash",
|
||||
"args": [
|
||||
"scripts/mcp_gitea.sh"
|
||||
],
|
||||
"env": {
|
||||
"GITEA_HOST": "https://axs.kuma1xn.vip",
|
||||
"GITEA_ACCESS_TOKEN_FILE": "/mnt/c/Users/uzuma/.codex/secrets/gitea-axs.token",
|
||||
"HTTPS_PROXY": "socks5://127.0.0.1:10808",
|
||||
"HTTP_PROXY": "socks5://127.0.0.1:10808"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
---
|
||||
name: Nexus 全面代码审计
|
||||
overview: 对 Nexus 6.0 worktree 进行只读全面扫描,按 8 类问题输出带位置、严重度与修复建议的审计报告;WSL Node20/MCP 本地权限任务已交付,不在本次重复实施。
|
||||
todos:
|
||||
- id: fix-p0-webssh
|
||||
content: "P0: WebSSH 强制 JWT server_id 绑定 + 失败时 close WebSocket"
|
||||
status: completed
|
||||
- id: fix-p1-xss
|
||||
content: "P1: servers/scripts 等 innerHTML 字段统一 esc()"
|
||||
status: pending
|
||||
- id: fix-p1-agent-apikey
|
||||
content: "P1: 收敛 Agent exec + API Key reveal + compare_digest"
|
||||
status: pending
|
||||
- id: fix-p2-heartbeat-stats
|
||||
content: "P2: 心跳仅 Redis + 在线统计与列表数据源一致"
|
||||
status: pending
|
||||
- id: fix-p2-hardening
|
||||
content: "P2: install 路由锁定、TOTP disable 二次验证、全局 JWT 中间件"
|
||||
status: pending
|
||||
- id: add-security-tests
|
||||
content: 补充 auth/webssh/agent 集成测试与 pip-audit CI
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Nexus 6.0 全面代码审计报告
|
||||
|
||||
**范围**: [`server/`](server/)、[`web/app/`](web/app/)、[`scripts/`](scripts/)、[`deploy/`](deploy/)、[`requirements.txt`](requirements.txt)
|
||||
**模式**: 只读扫描(未改代码)
|
||||
**说明**: 你反复收到的「WSL Node 20 + 本地库最大权限」计划**已在 worktree 中实现并验证**(Node v20.19.2、MYSQL_READONLY=false、GRANT ALL、MCP Connected)。请勿再重复执行该计划;下文为**新的**全项目审计。
|
||||
|
||||
**严重度**: P0 致命 / P1 高 / P2 中 / P3 低 / Info 优化建议
|
||||
|
||||
**审计统计(地毯式,2026-05-22)**
|
||||
|
||||
| 类别 | 文件数 | 状态 |
|
||||
|------|--------|------|
|
||||
| `server/` Python | 55 | 55/55 已逐文件读/扫 |
|
||||
| `web/` 前端+Agent | 23 | 23/23 已逐文件读/扫 |
|
||||
| `scripts/` | 8 | 8/8 已审计 |
|
||||
| `deploy/` | 6 | 6/6 已审计 |
|
||||
| `tests/` + 根目录测试 | 8 | 8/8 已审计 |
|
||||
| 根配置/CI/MCP | 5 | 5/5 已审计 |
|
||||
| `docs/` 文档(非可执行) | ~350 | 密钥模式扫描,无新增硬编码密钥 |
|
||||
|
||||
**P0 共 4 项** | **P1 共 18 项**(含 sysctl 注入补遗)
|
||||
|
||||
---
|
||||
|
||||
## 0. P0 / P1 问题总表(复查用)
|
||||
|
||||
### P0(4)
|
||||
|
||||
| ID | 文件 | 问题 |
|
||||
|----|------|------|
|
||||
| S-01 | `server/api/webssh.py` | 标准 JWT 无 `server_id` 时可 IDOR 连任意服务器 SSH |
|
||||
| S-02 | `server/api/agent.py` | `/api/agent/exec` + `subprocess_shell` + 全局 API Key = 中枢 RCE |
|
||||
| S-13 | `tests/load_test.py`, `tests/quick_load.py` | **仓库内硬编码 `msk-ufkvjm8...` API Key**(若生产密钥则灾难性泄露) |
|
||||
| S-14 | `web/agent/agent.py` | 子机 Agent `/exec` 同样 shell 执行;`verify_api_key` 用 `!=` 非常量时间 |
|
||||
|
||||
### P1(18,节选)
|
||||
|
||||
| ID | 文件 | 问题 |
|
||||
|----|------|------|
|
||||
| **S-19** | `server/application/services/sync_engine_v2.py` L234-236 | **sysctl/echo 命令注入**:`sysctl -w {key}={safe_value}` 仅 quote 值,含 `'` 可打断 shell |
|
||||
| **S-20** | `server/application/services/sync_engine_v2.py` L281-284 | **rollback 命令注入**:`backup_file` 未 quote 拼入 `cp ... && sysctl --system` |
|
||||
| S-03 | `server/api/settings.py` | `/api-key/reveal` 回读全局 API_KEY |
|
||||
| S-04 | `web/app/servers.html` | `category`/`agent_version` 未 esc → XSS |
|
||||
| S-05 | `web/app/scripts.html` | `category` 未 esc → XSS |
|
||||
| S-06 | `web/app/api.js` | JWT 存 localStorage |
|
||||
| S-07 | `web/app/index.html`, `terminal.html` | WS JWT 在 URL |
|
||||
| S-08 | `server/api/install.py` | 生产仍暴露无认证 install API |
|
||||
| S-09 | `server/api/auth.py` | TOTP disable 无二次验证 |
|
||||
| S-15 | `server/api/agent.py` | 全局 Key 可伪造任意无 per-key 服务器的心跳 |
|
||||
| S-16 | `web/install.php` | 遗留 PHP 安装面(与 install.html 双轨) |
|
||||
| S-17 | `server/main.py` + FastAPI | `/docs` `/openapi.json` 未鉴权暴露 API 结构 |
|
||||
| R-01 | `server/infrastructure/database/crypto.py` | AES-CBC 遗留 + 解密失败返回密文 |
|
||||
| E-01 | `server/main.py` | 无全局 JWT 中间件,仅靠各路由 Depends |
|
||||
| P-01 | `server/api/agent.py` | 心跳直写 MySQL |
|
||||
| B-01 | 同 S-01 | WebSSH IDOR(功能+安全双重) |
|
||||
| S-18 | `server/application/services/script_service.py` | 调用 Agent 用 `http://` 明文,MITM 可篡改命令 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 逐文件审计打勾(可执行代码 99 个)
|
||||
|
||||
### server/(55)
|
||||
|
||||
- [x] `server/__init__.py` 已审计 ✓ — 无问题
|
||||
- [x] `server/main.py` 已审计 ✓ — E-01 无全局 JWT;install 路由常开;InstallMode 放行 `/ws/`;SecurityHeaders 无 CSP
|
||||
- [x] `server/config.py` 已审计 ✓ — R-02 默认 `root:password` DATABASE_URL
|
||||
- [x] `server/api/__init__.py` 已审计 ✓
|
||||
- [x] `server/api/agent.py` 已审计 ✓ — **P0 S-02**;P-01;S-15;S-12 timing
|
||||
- [x] `server/api/assets.py` 已审计 ✓ — JWT 保护完整
|
||||
- [x] `server/api/auth.py` 已审计 ✓ — S-09 totp/disable
|
||||
- [x] `server/api/auth_jwt.py` 已审计 ✓ — PUBLIC_PREFIXES 未作全局中间件;逻辑正确
|
||||
- [x] `server/api/dependencies.py` 已审计 ✓ — S-10 危险命令仅警告
|
||||
- [x] `server/api/health.py` 已审计 ✓ — 公开合理
|
||||
- [x] `server/api/install.py` 已审计 ✓ — **S-08**;C-03 阻塞 subprocess;lock 无 JWT
|
||||
- [x] `server/api/schemas.py` 已审计 ✓ — Pydantic 校验良好
|
||||
- [x] `server/api/scripts.py` 已审计 ✓ — JWT;凭据脱敏需对照响应
|
||||
- [x] `server/api/search.py` 已审计 ✓ — LIKE 转义
|
||||
- [x] `server/api/servers.py` 已审计 ✓ — B-03/P-02;agent-key 返回明文 key(设计如此,需审计日志)
|
||||
- [x] `server/api/settings.py` 已审计 ✓ — **S-03** reveal
|
||||
- [x] `server/api/sync_v2.py` 已审计 ✓ — browse 用 shlex.quote
|
||||
- [x] `server/api/webssh.py` 已审计 ✓ — **P0 S-01**;B-02 未 close
|
||||
- [x] `server/api/websocket.py` 已审计 ✓ — S-07;P-03 多 worker 去重
|
||||
- [x] `server/application/__init__.py` 已审计 ✓
|
||||
- [x] `server/application/services/__init__.py` 已审计 ✓
|
||||
- [x] `server/application/services/auth_service.py` 已审计 ✓ — R-07 旧 refresh
|
||||
- [x] `server/application/services/script_service.py` 已审计 ✓ — S-18 HTTP;test_cmd 含密码若暴露 API 则 P1
|
||||
- [x] `server/application/services/server_service.py` 已审计 ✓
|
||||
- [x] `server/application/services/sync_engine_v2.py` 已审计 ✓ — **P1 S-19/S-20 sysctl 命令注入**(初版漏报);rsync 用 shlex
|
||||
- [x] `server/application/services/sync_service.py` 已审计 ✓
|
||||
- [x] `server/background/__init__.py` 已审计 ✓
|
||||
- [x] `server/background/heartbeat_flush.py` 已审计 ✓ — 与 B-04 应对齐
|
||||
- [x] `server/background/retry_runner.py` 已审计 ✓
|
||||
- [x] `server/background/schedule_runner.py` 已审计 ✓
|
||||
- [x] `server/background/self_monitor.py` 已审计 ✓
|
||||
- [x] `server/domain/__init__.py` 已审计 ✓
|
||||
- [x] `server/domain/models/__init__.py` 已审计 ✓ — 敏感列设计合理
|
||||
- [x] `server/domain/repositories/__init__.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/__init__.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/__init__.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/admin_repo.py` 已审计 ✓ — 登录限速仅 username+ip
|
||||
- [x] `server/infrastructure/database/audit_log_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/crypto.py` 已审计 ✓ — R-01;B-07
|
||||
- [x] `server/infrastructure/database/db_credential_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/migrations.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/password_preset_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/platform_node_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/push_schedule_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/script_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/server_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/session.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/setting_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/ssh_session_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/database/sync_log_repo.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/redis/__init__.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/redis/client.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/ssh/__init__.py` 已审计 ✓
|
||||
- [x] `server/infrastructure/ssh/asyncssh_pool.py` 已审计 ✓ — Info 压测
|
||||
- [x] `server/infrastructure/telegram/__init__.py` 已审计 ✓
|
||||
|
||||
### web/(23)
|
||||
|
||||
- [x] `web/app/api.js` 已审计 ✓ — S-06;U-01
|
||||
- [x] `web/app/layout.js` 已审计 ✓ — 搜索 esc 良好
|
||||
- [x] `web/app/login.html` 已审计 ✓ — S-06;CDN 无 SRI
|
||||
- [x] `web/app/index.html` 已审计 ✓ — S-07
|
||||
- [x] `web/app/servers.html` 已审计 ✓ — **S-04**
|
||||
- [x] `web/app/scripts.html` 已审计 ✓ — **S-05**
|
||||
- [x] `web/app/terminal.html` 已审计 ✓ — S-07
|
||||
- [x] `web/app/settings.html` 已审计 ✓ — U-03
|
||||
- [x] `web/app/install.html` 已审计 ✓ — S-08 无认证 install fetch
|
||||
- [x] `web/app/files.html` 已审计 ✓ — esc 一致
|
||||
- [x] `web/app/push.html` 已审计 ✓ — esc 良好
|
||||
- [x] `web/app/audit.html` 已审计 ✓
|
||||
- [x] `web/app/assets.html` 已审计 ✓
|
||||
- [x] `web/app/commands.html` 已审计 ✓ — S-11 onclick
|
||||
- [x] `web/app/credentials.html` 已审计 ✓ — esc 良好
|
||||
- [x] `web/app/schedules.html` 已审计 ✓
|
||||
- [x] `web/app/retries.html` 已审计 ✓ — esc 良好
|
||||
- [x] `web/favicon.svg` 已审计 ✓
|
||||
- [x] `web/install.php` 已审计 ✓ — **S-16** 遗留安装
|
||||
- [x] `web/agent/agent.py` 已审计 ✓ — **P0 S-14**
|
||||
- [x] `web/agent/agent.sh` 已审计 ✓
|
||||
- [x] `web/agent/install.sh` 已审计 ✓ — API key 进 config
|
||||
- [x] `web/agent/config.example.json` 已审计 ✓
|
||||
|
||||
### scripts/(8)deploy/(6)tests+根(8)配置(5)
|
||||
|
||||
- [x] `scripts/*.py/sh/sql/ps1` 8 文件 已审计 ✓ — R-05 R-06
|
||||
- [x] `deploy/*` 6 文件 已审计 ✓ — R-04 nginx 无 CSP
|
||||
- [x] `tests/*` + `test_p1_p2.py` 8 文件 已审计 ✓ — **P0 S-13** 硬编码密钥
|
||||
- [x] `requirements.txt` `.env.example` `.gitea/workflows/ci-cd.yml` `.cursor/mcp.json` `PROJECT_HANDOVER.md` 已审计 ✓ — R-08;CI 无 pip-audit
|
||||
|
||||
### docs/(~350,批量)
|
||||
|
||||
- [x] `docs/**` 已审计 ✓ — 全文 rg 密钥模式,无新增 `msk-` 类硬编码(与 tests 除外)
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能性缺陷(Bug)
|
||||
|
||||
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|
||||
|----|--------|------|------|----------|
|
||||
| B-01 | P1 | [`server/api/webssh.py`](server/api/webssh.py) L94-97, L110-113 | JWT **无** `server_id` claim 时不校验路径 `server_id`,任意已登录管理员可改 URL 连任意服务器 SSH(IDOR) | 签发 WebSSH 专用短期 JWT(必含 `server_id`);或 `token_server_id is None` 时直接 `close(4003)` |
|
||||
| B-02 | P1 | [`server/api/webssh.py`](server/api/webssh.py) L110-117 | 服务器不存在或凭据缺失时 `return` 未 `websocket.close()`,客户端挂起超时 | 统一 `accept` 后 `close(code, reason)` |
|
||||
| B-03 | P2 | [`server/api/servers.py`](server/api/servers.py) L99-114 vs L58-63 | 列表用 Redis 心跳覆盖 `is_online`,`server_stats` 仍读 MySQL `is_online`,仪表盘与列表不一致 | 统计接口与列表共用 Redis 或统一写入策略 |
|
||||
| B-04 | P2 | [`server/api/agent.py`](server/api/agent.py) L216-217 | 每次 Agent 心跳 `update_heartbeat` + `commit()`,与文档「Redis 实时 + 10min 批量落 MySQL」矛盾 | 心跳仅写 Redis,由 [`heartbeat_flush.py`](server/background/heartbeat_flush.py) 批量刷库 |
|
||||
| B-05 | P2 | [`server/api/agent.py`](server/api/agent.py) L259-260 | `TimeoutError` 时 `proc.kill()` 未 `await proc.wait()`,可能僵尸进程 | `kill()` + `await communicate()` 或 `asyncio.create_task` 清理 |
|
||||
| B-06 | P3 | [`web/app/api.js`](web/app/api.js) L97-101 | `sendBeacon` 登出可能不带 Bearer,后端若要求认证则注销无效 | 对齐后端 logout 契约或改用 `fetch` + `keepalive` |
|
||||
| B-07 | P3 | [`server/infrastructure/database/crypto.py`](server/infrastructure/database/crypto.py) L75-83 | 解密失败返回密文字符串,SSH 可能用错误密码连接且难排查 | 解密失败抛 `DecryptionError`,API 返回明确 5xx/配置错误 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 安全漏洞
|
||||
|
||||
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|
||||
|----|--------|------|------|----------|
|
||||
| S-01 | P0 | [`server/api/webssh.py`](server/api/webssh.py) + [`web/app/terminal.html`](web/app/terminal.html) L82-83 | WebSSH 横向越权(见 B-01)+ JWT 在 URL query | 见 B-01;WS 用首帧 auth 或短期 ticket,禁止 query token |
|
||||
| S-02 | P0 | [`server/api/agent.py`](server/api/agent.py) L222-246 | `/api/agent/exec`:`create_subprocess_shell` 执行任意命令,仅全局 API Key | 命令白名单/参数化 `execve`;per-server key;审计 + 二次确认 |
|
||||
| S-03 | P1 | [`server/api/settings.py`](server/api/settings.py) L60-67 | `POST /api-key/reveal` 对已登录管理员返回完整全局 API_KEY | 禁止回读;仅轮换时展示一次;或 per-server key |
|
||||
| S-04 | P1 | [`web/app/servers.html`](web/app/servers.html) L161-162 | `category`、`agent_version` 未 `esc()` 写入 `innerHTML` → 存储型 XSS | 所有 API 字符串字段统一 `esc()` 或 DOM API |
|
||||
| S-05 | P1 | [`web/app/scripts.html`](web/app/scripts.html) L36 附近 | `category` 列表渲染未转义 | 同 S-04 |
|
||||
| S-06 | P1 | [`web/app/api.js`](web/app/api.js) + [`login.html`](web/app/login.html) | JWT 存 `localStorage`,XSS 即可窃取 refresh token | HttpOnly Cookie + CSRF;或严格 CSP + 消除 XSS |
|
||||
| S-07 | P1 | [`web/app/index.html`](web/app/index.html) L208 | `/ws/alerts?token=` JWT 进 URL/日志/Referer | 同 S-01 WS 鉴权改造 |
|
||||
| S-08 | P1 | [`server/api/install.py`](server/api/install.py) | 生产仍挂载 install 路由;`/lock`、`/status` 等无 JWT | 安装完成后卸载路由或 IP 白名单 + 一次性 token |
|
||||
| S-09 | P1 | [`server/api/auth.py`](server/api/auth.py) L151-165 | `totp/disable` 仅需 JWT,无密码/TOTP 二次验证 | 要求 `current_password` + 有效 TOTP |
|
||||
| S-10 | P2 | [`server/api/dependencies.py`](server/api/dependencies.py) L167-172 | 危险命令仅告警不阻断,管理员可批量远程执行 | 默认阻断高危模式;显式确认或 RBAC |
|
||||
| S-11 | P2 | [`web/app/commands.html`](web/app/commands.html) L95,128 | `session_id` 嵌入 `onclick='...'` 未 JS 转义 | `data-*` + 事件委托 |
|
||||
| S-12 | P2 | [`server/api/agent.py`](server/api/agent.py) L48-66 | API Key 用 `==` 比较,非常量时间 | `secrets.compare_digest` |
|
||||
| S-13 | **P0** | [`tests/load_test.py`](tests/load_test.py) L18、[`tests/quick_load.py`](tests/quick_load.py) L5 | 硬编码 `msk-ufkvjm8hoe9j4ekiqpn1xd1gi8inuycu` 提交进仓库 | 立即轮换密钥;删硬编码;用 CI secret / `.env.test` |
|
||||
| S-14 | **P0** | [`web/agent/agent.py`](web/agent/agent.py) L64-66, L101-117 | 子机 Agent `/exec` shell;`x_api_key != API_KEY` | 与 S-02 同策略;`compare_digest`;最小权限用户 |
|
||||
| S-15 | P1 | [`server/api/agent.py`](server/api/agent.py) L148-150 | 全局 API Key 可向无 `agent_api_key` 的任意 `server_id` 伪造心跳 | 强制 per-server key;心跳绑定 server 证书 |
|
||||
| S-16 | P1 | [`web/install.php`](web/install.php) | 遗留 PHP 5 步安装,可写 `.env`/建表,与 FastAPI install 双轨 | 删除或 nginx deny;仅保留 install.html |
|
||||
| S-17 | P1 | [`server/main.py`](server/main.py) + FastAPI 默认 | `/docs` `/openapi.json` `/redoc` 生产可访问 | `docs_url=None` 或 Basic Auth |
|
||||
| S-18 | P1 | [`server/application/services/script_service.py`](server/application/services/script_service.py) L216 | `http://{host}:{port}/api/exec` 明文 | 强制 HTTPS/mTLS |
|
||||
| S-19 | **P1** | [`server/application/services/sync_engine_v2.py`](server/application/services/sync_engine_v2.py) L234-236 | `sysctl -w {key}={safe_value}` / `echo {key}={safe_value}`:仅 `shlex.quote(value)`,含 `'` 的 value 可逃逸 shell 引号执行任意命令 | `shlex.quote(f"{key}={value}")` 或 argv 列表不经 shell |
|
||||
| S-20 | **P1** | [`server/application/services/sync_engine_v2.py`](server/application/services/sync_engine_v2.py) L281-284 | `cp {backup_file} ... && sysctl --system`:`backup_file` 来自 `ls` 输出未转义 | 白名单路径 + `shlex.quote(backup_file)` |
|
||||
|
||||
**SQL 注入**: ORM/`select()` 为主,[`search.py`](server/api/search.py) 对 LIKE 通配符有转义,**未发现典型拼接 SQL 注入**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 代码规范违规
|
||||
|
||||
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|
||||
|----|--------|------|------|----------|
|
||||
| C-01 | P2 | 多处 `web/app/*.html` | 项目规则要求禁止未转义 `innerHTML`,但 [`servers.html`](web/app/servers.html)、[`scripts.html`](web/app/scripts.html) 等仍违规 | 抽公共 `renderTableRow()`,CI 检查 `innerHTML` 与 `${` 组合 |
|
||||
| C-02 | P2 | [`web/app/*.html`](web/app/) | 每页重复定义 `esc()`,[`layout.js`](web/app/layout.js) 有 `_esc()` 未统一导出 | `api.js` 导出 `esc`/`escAttr`,页面禁止私有副本 |
|
||||
| C-03 | P3 | [`server/api/install.py`](server/api/install.py) L283-284 | async 路由内同步 `subprocess.run` 阻塞事件循环 | `asyncio.to_thread` |
|
||||
| C-04 | P3 | 前端 | 部分页面裸 `fetch`(如 [`install.html`](web/app/install.html))绕过 `apiFetch` | 安装向导可例外,但应集中错误处理 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 安全风险(非直接漏洞)
|
||||
|
||||
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|
||||
|----|--------|------|------|----------|
|
||||
| R-01 | P1 | [`server/infrastructure/database/crypto.py`](server/infrastructure/database/crypto.py) | 遗留 AES-CBC + SHA256(API_KEY) 兼容 PHP | 迁移期后移除 CBC;统一 Fernet |
|
||||
| R-02 | P2 | [`server/config.py`](server/config.py) L40 附近 | `DATABASE_URL` 默认 `root:password` | 安装模式强制无默认密码;启动校验 |
|
||||
| R-03 | P2 | 15× [`web/app/*.html`](web/app/) | jsDelivr 无 SRI,供应链篡改可全站 RCE | 自托管 + `integrity` + `crossorigin` |
|
||||
| R-04 | P2 | [`deploy/nginx_https.conf`](deploy/nginx_https.conf) | 有 HSTS,无 CSP / X-Content-Type-Options | 逐步启用 CSP;`script-src` 限制 |
|
||||
| R-05 | P2 | [`scripts/sync_mysql_mcp_env.py`](scripts/sync_mysql_mcp_env.py) | 将 DB 密码写入 `.env` 的 `MYSQL_PASSWORD`,扩大泄露面 | MCP 独立只读/开发用户 |
|
||||
| R-06 | P2 | [`scripts/grant_nexus_local.sql`](scripts/grant_nexus_local.sql) | `GRANT ALL` + `'%'` 仅适合本地;误用于生产风险高 | 文档标注「仅本地」;生产用最小权限 SQL |
|
||||
| R-07 | P3 | [`server/application/services/auth_service.py`](server/application/services/auth_service.py) L147-153 | 旧 refresh token 格式仍兼容,重用检测弱于新格式 | 强制迁移、废弃旧 token |
|
||||
| R-08 | P3 | [`requirements.txt`](requirements.txt) | 无 CI `pip-audit`/OSV 扫描 | GitHub Action 定期依赖审计 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 性能问题
|
||||
|
||||
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|
||||
|----|--------|------|------|----------|
|
||||
| P-01 | P1 | [`server/api/agent.py`](server/api/agent.py) L216-217 | 心跳直写 MySQL(2000+ 服务器 × 60s) | 见 B-04 |
|
||||
| P-02 | P2 | [`server/api/servers.py`](server/api/servers.py) L58-63 | 列表页每服务器一次 `redis.hgetall`(N+1) | Redis pipeline / `MGET` 批量 |
|
||||
| P-03 | P2 | [`server/api/websocket.py`](server/api/websocket.py) L293-307 | 告警冷却 `_last_alert_time` 进程内 dict,多 worker 重复 Telegram | Redis SETNX + TTL 去重 |
|
||||
| P-04 | P3 | [`server/api/install.py`](server/api/install.py) | 安装阶段同步 subprocess | 见 C-03 |
|
||||
| P-05 | Info | [`server/infrastructure/ssh/asyncssh_pool.py`](server/infrastructure/ssh/asyncssh_pool.py) | 连接池设计合理;需压测 2000+ 并发 SSH 上限 | 配置 max pool + 监控指标 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 可用性问题
|
||||
|
||||
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|
||||
|----|--------|------|------|----------|
|
||||
| U-01 | P2 | [`web/app/api.js`](web/app/api.js) L35-37 | 空 `catch` 吞掉刷新/加载错误 | 统一 `toast('error', ...)` 或重试提示 |
|
||||
| U-02 | P2 | [`server/infrastructure/database/crypto.py`](server/infrastructure/database/crypto.py) | 密钥错误时表现为 SSH 连接失败而非配置错误 | 见 B-07 |
|
||||
| U-03 | P3 | [`web/app/settings.html`](web/app/settings.html) | 部分配置加载失败静默 | 与 U-01 统一错误 UX |
|
||||
| U-04 | Info | 移动端 | [`layout.js`](web/app/layout.js) 已有 overlay;大表横向滚动需各页验证 | 关键页 E2E 截图验收 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 可扩展性问题
|
||||
|
||||
| ID | 严重度 | 位置 | 描述 | 修复建议 |
|
||||
|----|--------|------|------|----------|
|
||||
| E-01 | P1 | 认证架构 | 无全局 JWT 中间件,每路由靠 `Depends(get_current_admin)`,新路由易漏 | `main.py` 默认拒绝 + 白名单(install/health/agent/auth/login) |
|
||||
| E-02 | P2 | Agent 协议 | 全局单 Key + shell exec,难以多租户/多环境隔离 | per-server API key + scoped capabilities |
|
||||
| E-03 | P2 | 配置三写 | install → `.env` + `config.php` + MySQL `settings`,漂移风险 | 单一真相源 + 同步脚本(已有部分,需校验任务) |
|
||||
| E-04 | P3 | 前端 | 15 页独立 Alpine 内联脚本,无组件复用 | 逐步抽 `components/` 或轻量模块(非必须大重构) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 优化点(非错误)
|
||||
|
||||
| ID | 位置 | 建议 |
|
||||
|----|------|------|
|
||||
| O-01 | [`server/api/search.py`](server/api/search.py) | LIKE 转义已做,可补全文索引或 ES(2000+ 资产搜索) |
|
||||
| O-02 | [`web/app/layout.js`](web/app/layout.js) | 全局搜索已统一 `_esc()`,推广为唯一转义入口 |
|
||||
| O-03 | [`server/background/heartbeat_flush.py`](server/background/heartbeat_flush.py) | 与 B-04 对齐后,可加批量大小/退避指标 |
|
||||
| O-04 | 测试 | [`tests/`](tests/) 覆盖有限,优先补 auth/webssh/agent 集成测试 |
|
||||
| O-05 | 文档 | 已有 [`docs/project/mysql-mcp-setup.md`](docs/project/mysql-mcp-setup.md);安全项可增 ADR(WebSSH JWT、Agent exec) |
|
||||
|
||||
---
|
||||
|
||||
## 架构风险总览
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph attack [高优先级攻击面]
|
||||
XSS[XSS localStorage]
|
||||
WebSSH[IDOR WebSSH]
|
||||
AgentExec[Agent shell exec]
|
||||
APIKey[Global API_KEY reveal]
|
||||
end
|
||||
XSS --> TokenTheft[窃取 JWT]
|
||||
TokenTheft --> WebSSH
|
||||
TokenTheft --> APIKey
|
||||
APIKey --> AgentExec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 修复优先级(建议执行顺序)
|
||||
|
||||
1. **立即(P0/P1 安全)**: S-01/B-01 WebSSH 绑定;S-04/S-05 XSS;S-02 Agent exec 收敛;S-03 API Key reveal;**S-19/S-20 sync_engine sysctl 注入**
|
||||
2. **本周(一致性与性能)**: B-04/P-01 心跳写路径;B-03 在线统计;P-02 Redis pipeline
|
||||
3. **迭代(纵深防御)**: S-06/S-07 会话与 WS;R-03/R-04 CDN/CSP;E-01 全局鉴权中间件
|
||||
4. **文档/流程**: O-04 测试;R-08 依赖审计;每项修复写 [`docs/changelog/`](docs/changelog/)
|
||||
|
||||
---
|
||||
|
||||
## 与「WSL Node 20」计划的关系
|
||||
|
||||
| 计划项 | 状态 |
|
||||
|--------|------|
|
||||
| `wsl_install_node20.sh` 代理修复 | 已实现 |
|
||||
| `MYSQL_READONLY=false` + `--writable` | 已实现 |
|
||||
| `grant_nexus_local.sql` | 已实现 |
|
||||
| MCP/Node 验证 | 已通过 |
|
||||
|
||||
**无需再跑该安装计划。** 若 MCP 仍红,仅需 Cursor Reload + 检查 MySQL 服务,而非重复改代码。
|
||||
|
||||
---
|
||||
|
||||
## 下一步(需你确认后执行)
|
||||
|
||||
若你确认本审计报告,可在 **Agent 模式** 按优先级分批修复(建议先做 P0/P1 安全项,每项单独 changelog)。你可指定:
|
||||
|
||||
- 只修安全(S-01 ~ S-12)
|
||||
- 或「P0/P1 全部」
|
||||
- 或指定 ID 列表
|
||||
|
||||
我不会再自动重复 WSL Node20 实施循环。
|
||||
|
||||
---
|
||||
|
||||
## 10. 如何强制「逐行审查」(Phase 2 操作规程)
|
||||
|
||||
初版(Phase 1)= **攻击面抽查 + 文件打勾**,不满足「逐行」承诺。要可复查、少漏报,必须单独开 **Phase 2**,并按下列规则验收。
|
||||
|
||||
### 10.1 你对助手说什么(复制即用)
|
||||
|
||||
切换到 **Agent 模式**(非 Plan),分批发:
|
||||
|
||||
```
|
||||
执行审计计划 Phase 2:逐行复核。禁止攻击面抽查代替。
|
||||
|
||||
范围:server/**/*.py(本轮仅 server,完成后再 web)。
|
||||
|
||||
方法:
|
||||
1. 用 rg 列出本目录每个文件的:命令拼接、subprocess、exec_ssh、WebSocket、SQL text(、硬编码密钥
|
||||
2. 对每个命中行 Read 上下文 ±15 行,判定 safe / finding
|
||||
3. 每个文件输出:
|
||||
- [path] 行数 N | rg 命中 M 条 | 全部 closure 后才能标「逐行完成 ✓」
|
||||
- findings 表:行号 | 严重度 | 描述
|
||||
4. 禁止写「已审计」除非该文件 M=0 或每条命中都有 closure 记录
|
||||
5. 每完成 5 个文件汇报一次,不要一次声称全库完成
|
||||
|
||||
从 server/application/services/sync_engine_v2.py 开始(已知漏报)。
|
||||
```
|
||||
|
||||
修代码与审计**分两次会话**,避免混在一起。
|
||||
|
||||
### 10.2 「逐行」的可验收定义(不是读每一行散文)
|
||||
|
||||
| 必须做 | 说明 |
|
||||
|--------|------|
|
||||
| 全文 Read 或按 200 行分段 Read 至 EOF | 每个 `.py` 至少覆盖 100% 行号范围 |
|
||||
| 全文件 rg 规则表(见 10.3) | 每个命中 = 一条待 closure 项 |
|
||||
| 外部输入追调用链 | API → service → ssh/subprocess 到顶 |
|
||||
| 产出 `docs/reports/audit-line-coverage-*.md` | 复查 AI 只认文件:行,不认「✓」 |
|
||||
|
||||
**不算逐行**:只 grep 几个关键词就标 ✓;只读 Top 15;子代理汇总不落行号。
|
||||
|
||||
### 10.3 每个 Python 文件必跑的 rg 规则(closure 前不得标完成)
|
||||
|
||||
```bash
|
||||
# 在单个文件或目录执行,命中全部要 Read 上下文
|
||||
rg -n "f[\"'].*\{.*\}.*(exec|run|shell|ssh|sysctl|curl|wget|sudo|bash|sh -c)" server/
|
||||
rg -n "subprocess\.|create_subprocess_shell|os\.system|popen" server/
|
||||
rg -n "exec_ssh_command|conn\.run\(|asyncssh" server/
|
||||
rg -n "text\(|execute\(|raw\(" server/
|
||||
rg -n "Depends\(get_current_admin\)|APIRouter|@router\.(get|post|put|delete|websocket)" server/api/
|
||||
rg -n "HTTPException|401|403|API_KEY|compare_digest|secrets\." server/
|
||||
```
|
||||
|
||||
前端另表:`innerHTML`、`localStorage`、`WebSocket.*token`、`eval(`。
|
||||
|
||||
### 10.4 推荐拆分顺序(避免一次吹「全库完成」)
|
||||
|
||||
| 批次 | 路径 | 约行数 |
|
||||
|------|------|--------|
|
||||
| 2a | `server/api/*.py` | 高 |
|
||||
| 2b | `server/application/services/*.py` | 高(含 sync_engine) |
|
||||
| 2c | `server/infrastructure/**/*.py` | 中 |
|
||||
| 2d | `server/background/*.py` + `main.py` + `config.py` | 中 |
|
||||
| 2e | `web/app/*.{html,js}` | 高 |
|
||||
| 2f | `web/agent/*` + `scripts/*` + `tests/*` | 中 |
|
||||
|
||||
每批结束交付:`audit-phase-2{X}.md`(仅 findings + closure 表)。
|
||||
|
||||
### 10.5 你可加的 Cursor 规则(防止再抽查)
|
||||
|
||||
在 `.cursor/rules/audit-line-review.mdc` 中写:
|
||||
|
||||
- 安全审计任务:**禁止**在未列出 `文件:行号` 的情况下声称完成
|
||||
- **禁止**使用「已审计 ✓」除非附带 rg 命中 closure 表
|
||||
- 发现命令拼接必须引用 `shlex`/argv 或标为 finding
|
||||
- 单次回复最多声称完成 **5 个文件** 的逐行审查
|
||||
|
||||
### 10.6 你怎么验收助手是否真的逐行
|
||||
|
||||
1. 抽查:打开 `sync_engine_v2.py`,看报告是否含 **234、236、284**(S-19/S-20)
|
||||
2. 要求给 `wc -l` 与「本文件 Read 行范围 1-N」
|
||||
3. 要求 `rg 命中数 = closure 数`,差 1 条即打回
|
||||
4. 用另一只 AI 只喂 `audit-phase-*.md` + 源码,做交叉验证
|
||||
|
||||
### 10.7 Phase 2 todos(待你确认后执行)
|
||||
|
||||
| id | 内容 |
|
||||
|----|------|
|
||||
| phase2-api | `server/api/*.py` 全文 + rg closure |
|
||||
| phase2-services | `server/application/services/*.py` 含 sync_engine 全行 |
|
||||
| phase2-infra | `server/infrastructure/**` |
|
||||
| phase2-web | `web/app` innerHTML + token |
|
||||
| phase2-report | 合并为 `docs/reports/audit-line-coverage-2026-05-22.md` |
|
||||
|
||||
---
|
||||
|
||||
## 11. 逐行走读标准(SSOT — 唯一验收依据)
|
||||
|
||||
**独立文件(已落地)**:
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| [`docs/project/line-walk-audit-standard.md`](docs/project/line-walk-audit-standard.md) | 完整标准(推荐给人与复查 AI) |
|
||||
| [`.cursor/rules/audit-line-review.mdc`](.cursor/rules/audit-line-review.mdc) | Cursor 编辑 `server/`/`web/` 时自动加载 |
|
||||
|
||||
本节是 Phase 2 的**正式标准**(与上表同步)。与 Phase 1(攻击面抽查)严格区分;未满足 **DoD** 的文件不得标记「逐行完成」。
|
||||
|
||||
### 11.1 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
|------|------|
|
||||
| **走读** | 对单文件按行号顺序阅读源码(可分段),并对规则命中点做上下文分析 |
|
||||
| **触达** | 仅打开/ grep 过文件,未满足走读 DoD |
|
||||
| **规则命中** | `rg` 规则表(11.4)在某行产生的匹配 |
|
||||
| **Closure** | 对一条规则命中的判定记录:`SAFE` 或 `FINDING`(含行号、理由) |
|
||||
| **DoD** | 单文件完成定义(11.6),全部满足才可标「逐行完成 ✓」 |
|
||||
|
||||
### 11.2 适用范围
|
||||
|
||||
| 包含 | 排除 |
|
||||
|------|------|
|
||||
| `server/**/*.py` | `docs/**` 仅做密钥扫描(11.8) |
|
||||
| `web/app/*.{html,js}` | `.cursor/skills/**` |
|
||||
| `web/agent/**` | 第三方 minified 库 |
|
||||
| `scripts/**`、`deploy/**`、`tests/**` | 图片、字体、lock 文件 |
|
||||
|
||||
### 11.3 单文件走读流程(强制 8 步)
|
||||
|
||||
对清单中**每一个**源文件依次执行,不得跳步:
|
||||
|
||||
```
|
||||
Step 1 登记:记录 path、语言、wc -l 行数 N
|
||||
Step 2 全文覆盖:Read offset=1, limit=200 循环至 EOF(末段 offset 必须 ≥ N-5)
|
||||
Step 3 规则扫描:对该文件跑 11.4 中「本语言」全部规则,得到命中列表 H[]
|
||||
Step 4 Closure:对 H[] 中每条命中 Read ±15 行,填写 11.5 表格一行
|
||||
Step 5 入口梳理:若为 API/路由/WS,列出对外入口表(方法、路径、鉴权方式)
|
||||
Step 6 数据流:标出「外部输入 → 危险 sink」(SQL/shell/文件/innerHTML/出站请求)
|
||||
Step 7 八类核对:按 11.7 勾选本文件涉及的类别(无则勾 NONE)
|
||||
Step 8 DoD 检查:满足 11.6 后写入报告,标记「逐行完成 ✓」
|
||||
```
|
||||
|
||||
**单会话上限**:最多完成 **5 个文件** 的 Step 1–8,防止敷衍。
|
||||
|
||||
### 11.4 规则表(必须 100% Closure)
|
||||
|
||||
#### Python(`server/`、`scripts/`、`tests/`、`web/agent/*.py`)
|
||||
|
||||
| 规则 ID | rg 模式(示意) | 审查要点 |
|
||||
|---------|----------------|----------|
|
||||
| PY-01 | `f["'].*\{.*\}.*` 且行内含 exec/run/shell/ssh/sysctl/curl/sudo/bash | 是否经 shell?变量是否 shlex.quote **整参数**? |
|
||||
| PY-02 | `subprocess\.|create_subprocess_shell|os\.system|popen` | shell=True?命令来源? |
|
||||
| PY-03 | `exec_ssh_command|conn\.run\(` | asyncssh 是否默认 shell?拼接方式? |
|
||||
| PY-04 | `text\(|execute\(|raw\(` | SQL 是否拼接用户输入? |
|
||||
| PY-05 | `@router\.|APIRouter|websocket` | 是否有 `Depends(get_current_admin)` 或等效? |
|
||||
| PY-06 | `API_KEY|SECRET|password|token` 字面量 | 是否硬编码密钥? |
|
||||
| PY-07 | `pickle|yaml\.load\(|eval\(|exec\(` | 反序列化 / 动态执行 |
|
||||
| PY-08 | `==.*[Kk]ey|!=.*[Kk]ey` | API Key 比较是否 timing-safe |
|
||||
| PY-09 | `except.*:.*pass|except Exception` | 是否吞掉安全相关异常 |
|
||||
| PY-10 | `open\(|Path\(.*write|unlink` | 路径遍历、任意写 |
|
||||
|
||||
#### 前端(`web/app/*.{html,js}`)
|
||||
|
||||
| 规则 ID | rg 模式 | 审查要点 |
|
||||
|---------|---------|----------|
|
||||
| FE-01 | `innerHTML|outerHTML|insertAdjacentHTML` | 每个 `${}` 是否 esc / textContent |
|
||||
| FE-02 | `localStorage|sessionStorage` | 是否存 JWT/密钥 |
|
||||
| FE-03 | `WebSocket.*token|\.token=` | Token 是否在 URL |
|
||||
| FE-04 | `fetch\(|apiFetch` | 是否绕过统一鉴权/错误处理 |
|
||||
| FE-05 | `onclick=.*\$\{|javascript:` | 属性上下文注入 |
|
||||
| FE-06 | `cdn\.|script src=` | SRI、供应链 |
|
||||
|
||||
#### Shell / SQL / Deploy
|
||||
|
||||
| 规则 ID | 范围 | 要点 |
|
||||
|---------|------|------|
|
||||
| SH-01 | `*.sh` | 未引用变量、curl 明文密钥 |
|
||||
| DP-01 | `deploy/nginx*.conf` | CSP、TLS、限流 |
|
||||
| SQL-01 | `*.sql` | GRANT 过大、'%' host |
|
||||
|
||||
**无命中**:在 Closure 表写 `H=0(规则表零命中)`,仍须完成 Step 2 全文 Read。
|
||||
|
||||
### 11.5 Closure 记录格式(每条命中一行)
|
||||
|
||||
```markdown
|
||||
| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由(≤1句) |
|
||||
|------|------|------|------|--------|--------------|
|
||||
| sync_engine_v2.py | 236 | PY-01 | FINDING | P1 | sysctl -w 仅 quote value,' 可断 shell |
|
||||
| sync_engine_v2.py | 95 | PY-01 | SAFE | — | rsync 路径双端 shlex.quote |
|
||||
```
|
||||
|
||||
- **SAFE** 必须写清依据(如「整段 argv」「Pydantic 校验」「仅常量」)
|
||||
- **FINDING** 必须对应 11.7 八类之一,并给修复方向(≤1 句)
|
||||
|
||||
### 11.6 单文件 DoD(全部打勾才算完成)
|
||||
|
||||
- [ ] Step 2:Read 覆盖行号 `1 .. N`(报告注明 N 与最后 Read 的 offset/limit)
|
||||
- [ ] Step 3–4:`len(H) == Closure 行数`(含 SAFE + FINDING)
|
||||
- [ ] Step 5:有对外入口则必有「入口表」;纯 `__init__.py` 可写 N/A
|
||||
- [ ] Step 6:已标外部输入与 sink,或写「无外部输入」
|
||||
- [ ] Step 7:八类核对已勾
|
||||
- [ ] 报告含「逐行完成 ✓」且**无** Phase 1 式笼统描述
|
||||
|
||||
### 11.7 问题八类与严重度(与 KPI 对齐)
|
||||
|
||||
| 类别 | 代号 | P0 示例 | P1 示例 | P2 示例 |
|
||||
|------|------|---------|---------|---------|
|
||||
| 功能缺陷 | BUG | — | 统计与列表数据源不一致 | 空 catch 吞错误 |
|
||||
| 安全漏洞 | VULN | 未授权 RCE、IDOR 拿 Shell | XSS、密钥回读、命令注入 | CSRF 缺失 |
|
||||
| 规范违规 | STYLE | — | 违反项目 esc 规则 | 重复 esc 实现 |
|
||||
| 安全风险 | RISK | 仓库内生产 API Key | 明文 HTTP 调 Agent、弱 crypto | 无 SRI |
|
||||
| 性能 | PERF | — | 心跳直写 MySQL、Redis N+1 | 阻塞 subprocess |
|
||||
| 可用性 | UX | — | — | 加载失败无提示 |
|
||||
| 可扩展性 | EXT | — | 无全局鉴权中间件 | 配置三写漂移 |
|
||||
| 优化点 | OPT | — | — | 可合并重复逻辑 |
|
||||
|
||||
**KPI(复查用)**:P0 漏报 -100 / P1 漏报 -50;每条 FINDING 必须 `文件:行号` 可定位。
|
||||
|
||||
### 11.8 批次交付物模板
|
||||
|
||||
每个批次一个文件:`docs/reports/audit-phase-2{X}-line-walk.md`
|
||||
|
||||
```markdown
|
||||
# Phase 2{X} 逐行走读 — {目录}
|
||||
|
||||
## 文件清单
|
||||
| 文件 | 行数 N | 命中 H | Closure | 状态 |
|
||||
|------|--------|--------|---------|------|
|
||||
|
||||
## Findings(仅 FINDING)
|
||||
| ID | 文件:行 | 类 | 度 | 描述 | 修复建议 |
|
||||
|
||||
## Closure 全表(含 SAFE)
|
||||
(11.5 格式)
|
||||
|
||||
## 入口表(api 批次必填)
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|
||||
## 走读签字
|
||||
- 范围:...
|
||||
- 完成文件数 / 计划文件数
|
||||
- 未含「攻击面 Top N」字样:是/否
|
||||
```
|
||||
|
||||
### 11.9 禁止行为(违反即打回)
|
||||
|
||||
1. 未列 `文件:行号` 的 FINDING
|
||||
2. `rg 命中数 ≠ Closure 数` 仍标完成
|
||||
3. 用子代理「Top 15」代替 Closure 全表
|
||||
4. 只写「已审计 ✓」无 Step 2 行号范围
|
||||
5. 单回复声称完成整库 `server/` 或全部 `web/`
|
||||
6. 审计与修代码同一任务混合(除非用户明确要求)
|
||||
|
||||
### 11.10 与 Phase 1 的关系
|
||||
|
||||
| Phase 1 | Phase 2 |
|
||||
|---------|---------|
|
||||
| 攻击面抽查、快速风险图 | 逐行走读、可复查 closure |
|
||||
| 可发现「显眼」P0 | 可发现「单文件局部」拼接类 P1(如 sysctl) |
|
||||
| 报告在计划 §0–§8 | 报告在 `docs/reports/audit-phase-2*.md` |
|
||||
|
||||
**原则**:Phase 1 的 FINDING 在 Phase 2 须逐条 **VERIFY**(行号仍成立或降级/关闭);Phase 2 可新增 FINDING,不得删减已确认行号而不说明。
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
description: 减少盲目搜文件、控制 Read 次数与 token 消耗
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Agent 探索约束(省 token)
|
||||
|
||||
## 开始前
|
||||
|
||||
1. 读根目录 `AGENTS.md` 的「代码地图」与「问题 → 文件」表
|
||||
2. 架构/API 全览只读 `docs/project/nexus-functional-development-guide.md`(或用户指定的 §),禁止为找结构全库 Grep
|
||||
3. 用户已给出路径/文件名时,只读该路径,禁止扩大范围
|
||||
|
||||
## 搜索顺序
|
||||
|
||||
```
|
||||
AGENTS.md / 功能指南 § → Grep(带 path)→ Read(≤3 个文件)→ 仍不够再扩一圈
|
||||
```
|
||||
|
||||
- **禁止**无 `path` 的全仓库 `Grep` / `Glob`(除非用户明确要求全库)
|
||||
- **Grep 优先于 Read**;Glob 只用于定位文件名,不逐个 Read 全部匹配
|
||||
- 同一任务 **Read 不超过 5 个文件**;找到答案后立即停止
|
||||
- 不要重复 Read 已在本对话出现过的文件
|
||||
|
||||
## 分层定位(禁止跨层乱搜)
|
||||
|
||||
| 层 | 路径 |
|
||||
|----|------|
|
||||
| HTTP 路由 | `server/api/<module>.py` |
|
||||
| 业务 | `server/application/services/` |
|
||||
| 数据访问 | `server/infrastructure/` |
|
||||
| 模型 | `server/domain/models/` |
|
||||
| 前端页 | `frontend/src/pages/` |
|
||||
| 前端路由 | `frontend/src/router/index.ts` |
|
||||
| 前端 API 客户端 | `frontend/src/api/` |
|
||||
|
||||
真实前端源码在 `frontend/src/`,不是 `web/app/*.html`(那是构建产物)。
|
||||
|
||||
## 文档索引
|
||||
|
||||
- 功能 SSOT:`docs/project/nexus-functional-development-guide.md`
|
||||
- 文档导航:`docs/README.md`
|
||||
- 本地路径/MCP:`docs/project/linux-dev-paths.md`
|
||||
- 运维(连机/exec):用户 Skill `Nexus平台` 或 `docs/project/nexus-1panel-operations-knowledge.md`
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
description: 安全/质量逐行走读审计 — Phase 2 closure 流程;禁止攻击面抽查冒充全量审计
|
||||
globs:
|
||||
- server/**
|
||||
- web/app/**
|
||||
- web/agent/**
|
||||
- scripts/**
|
||||
- deploy/**
|
||||
- tests/**
|
||||
---
|
||||
|
||||
# 逐行走读审计规则(Phase 2)
|
||||
|
||||
完整标准见:`standards/line-walk-audit-standard-v2.md`(权威)· Nexus SSOT:`docs/project/line-walk-audit-standard.md` · 转接索引:`docs/project/standards-transfer-package.md`
|
||||
|
||||
## 何时启用
|
||||
|
||||
用户要求「全量审计」「逐行审查」「走读」「Phase 2」「安全审计」时,**必须**遵守本规则,不得用攻击面 Top N 或子代理汇总代替。
|
||||
|
||||
## 强制流程(每文件 8 步)
|
||||
|
||||
1. 登记 `path`、语言、行数 N(`wc -l`)
|
||||
2. **全文 Read** 至 EOF(可每 200 行分段;末段须覆盖 N)
|
||||
3. 对该文件跑 `docs/project/line-walk-audit-standard.md` §4 全部适用规则 → 命中列表 H
|
||||
4. 对 H **每一条** Read ±15 行,填 Closure(SAFE / FINDING + 行号 + 理由)
|
||||
5. API/WS 文件:写入口表(方法、路径、鉴权)
|
||||
6. 标外部输入 → sink(SQL / shell / 文件 / innerHTML / 出站 HTTP)
|
||||
7. 八类归类:BUG / VULN / STYLE / RISK / PERF / UX / EXT / OPT
|
||||
8. 满足 DoD 后标「逐行完成 ✓」
|
||||
|
||||
**单会话上限**:最多 **5 个文件** 完成 8 步。
|
||||
|
||||
## DoD(缺一不可)
|
||||
|
||||
- `len(H) == Closure 行数`(H=0 须写明「规则零命中」)
|
||||
- Read 覆盖 `1..N`(报告写明 N 与 Read 范围)
|
||||
- 每条 FINDING 含 **`文件:行号`**
|
||||
- 交付 `docs/reports/audit-phase-2*-line-walk.md`(含 Closure **全表**,含 SAFE)
|
||||
|
||||
## 命令拼接(高频漏报)
|
||||
|
||||
- `f"{key}={shlex.quote(value)}"` **不等于**安全:须 `shlex.quote(f"{key}={value}")` 或 argv 不经 shell
|
||||
- `ls`/`find` 输出拼进 `cp`/`rm` 须 **白名单 + shlex.quote**
|
||||
- `exec_ssh_command` / `conn.run(command)` 默认经远程 shell
|
||||
|
||||
## 禁止
|
||||
|
||||
- 无行号的 FINDING
|
||||
- 「已审计 ✓」无 Closure 表
|
||||
- 单回复声称整库 `server/` 或全部 `web/` 完成
|
||||
- 子代理只出 Top 15 不落行号
|
||||
- 同一任务混审计与改代码(除非用户明确要求)
|
||||
|
||||
## 严重度(复查 KPI)
|
||||
|
||||
- **P0**:未授权 RCE、IDOR Shell、仓库内生产密钥等
|
||||
- **P1**:需认证后的命令注入、XSS、密钥回读等
|
||||
- P0 漏报 -100 / P1 漏报 -50(用户复查约定)
|
||||
|
||||
## 报告路径
|
||||
|
||||
| 用途 | 路径 |
|
||||
|------|------|
|
||||
| 标准 SSOT | `docs/project/line-walk-audit-standard.md` |
|
||||
| 批次产出 | `docs/reports/audit-phase-2{a-f}-line-walk.md` |
|
||||
| Phase 1 参考 | `.cursor/plans/nexus_全面代码审计_a33e6fc2.plan.md` §0–§8 |
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
description: Nexus 前端规范 — Tailwind v4 + Alpine.js;api.js JWT;禁止 XSS
|
||||
globs: web/app/**
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Nexus 前端 (Tailwind + Alpine.js)
|
||||
|
||||
## 页面结构
|
||||
|
||||
- 业务页先引入:`/app/api.js` → `/app/layout.js`(侧栏、搜索、移动端 overlay)
|
||||
- 15 个 HTML 页面同级放在 `web/app/`,保持导航一致
|
||||
|
||||
## API 调用
|
||||
|
||||
- 一律 `apiFetch()` / `apiHeadersJSON()`,禁止手写裸 `fetch` 漏 Authorization
|
||||
- Token 存 `localStorage`:`access_token`、`refresh_token`、`token_expires`
|
||||
- 到期前 2 分钟自动 refresh;401 重试一次后 `_logout()`
|
||||
- 8 小时无活动会话超时(与后端 JWT `updated_at` 对齐)
|
||||
|
||||
## WebSocket
|
||||
|
||||
- 仪表盘 `/ws/alerts?token=...`(JWT 查询参数,ADR-011)
|
||||
- 收到 `alert` / `recovery` 刷新 stats,勿阻塞 UI 线程
|
||||
|
||||
## 安全(XSS)
|
||||
|
||||
- 禁止 `${userInput}` 直接插入 `innerHTML` 或 Alpine `x-html`
|
||||
- 用户/服务器名等用 `textContent` 或 Alpine 文本绑定
|
||||
|
||||
## WebSSH
|
||||
|
||||
- `terminal.html`:xterm.js + Koko 协议;resize 随窗口
|
||||
- 关闭码 4003 = 凭据/授权失败
|
||||
|
||||
## 验收
|
||||
|
||||
改 UI 后浏览器实机验证:Console 无报错、Network 无 404/500、登录→核心流程可走通。
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
description: 远程 Linux 路径必须用 POSIX,禁止 os.path 拼接
|
||||
globs: server/**/*.py
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 远程路径 = POSIX only
|
||||
|
||||
## 规则
|
||||
|
||||
- **SSH / SFTP / rsync 目标路径**(含 `target_path`、`remote_path`、命令里的文件路径):
|
||||
- 使用 `server.utils.posix_paths`(`normalize_remote_abs_path`、`remote_join`、`posix_join` 等)
|
||||
- **禁止** `os.path.join` / `dirname` / `basename` / `normpath` 处理这类字符串
|
||||
- **Nexus 本机真实 I/O**(`open`、`os.walk`、`os.scandir`):可用 `os.path`,逻辑拼接仍优先 `posix_join`
|
||||
- **仓库本地路径**(`Path(__file__)`、MCP `DEPLOY_DIR`):可用 `os.path` / `pathlib`
|
||||
|
||||
## 参考
|
||||
|
||||
- 实现:`server/utils/posix_paths.py`
|
||||
- 审计:`docs/audit/2026-06-01-posix-path-batch-review.md`
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
description: Nexus Python/FastAPI 后端规范 — 异步、Pydantic、Repository、UTC 时间
|
||||
globs: server/**/*.py
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Nexus Python 后端
|
||||
|
||||
## 依赖版本(勿随意降级)
|
||||
|
||||
FastAPI 0.115 · SQLAlchemy 2.0 async · aiomysql · Pydantic v2 · asyncssh 2.17 · redis 5.x · PyJWT 2.10
|
||||
|
||||
## API 路由
|
||||
|
||||
- 请求/响应模型定义在 `server/api/schemas.py`
|
||||
- 业务 API 必须 `Depends(get_current_admin)`;Agent 用 `X-API-Key`;install 路由无 JWT
|
||||
- 返回结构化错误,禁止裸 `except: pass`
|
||||
|
||||
## 数据库
|
||||
|
||||
- 模型:`server/domain/models/__init__.py`(14 表)
|
||||
- 访问:`*_repo.py`,分页在 Repository 层 `offset/limit/count`
|
||||
- 时间戳:`datetime.now(timezone.utc)`,禁止 naive datetime
|
||||
- 连接池:默认 pool=160、overflow=120(安装向导按 MySQL `max_connections` 计算)
|
||||
|
||||
## Redis
|
||||
|
||||
- 客户端:`server/infrastructure/redis/client.py`,启动时强校验(ADR-009)
|
||||
- 心跳 key:`heartbeat:{server_id}` TTL 600;告警 key:`alerts:{server_id}` TTL 3600
|
||||
- 跨 worker 广播:Redis Pub/Sub `nexus:alerts`
|
||||
|
||||
## SSH
|
||||
|
||||
- 仅用 **asyncssh**(已删除 paramiko)
|
||||
- 连接池引用计数:`acquire` / `release`,空闲 5min 清理
|
||||
- 远程命令参数必须 `shlex.quote()`
|
||||
|
||||
## 凭据加密
|
||||
|
||||
- Fernet 存储 `Server.password` / `ssh_key_private`
|
||||
- API 响应:`password_set` 布尔,禁止返回明文密码
|
||||
|
||||
## 测试
|
||||
|
||||
- `pytest` + `tests/conftest.py`;改 API/Service 须跑相关测试
|
||||
- 新 endpoint 至少覆盖:认证失败、校验失败、成功路径
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
description: Nexus 安全规范 — JWT/TOTP/API Key/Fernet/CORS;MCP MySQL 只读
|
||||
globs: server/api/**,server/application/**,server/infrastructure/database/**,web/app/**
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Nexus 安全
|
||||
|
||||
## 认证矩阵
|
||||
|
||||
| 调用方 | 机制 |
|
||||
|--------|------|
|
||||
| 管理端 API | JWT Bearer + 可选 TOTP |
|
||||
| Agent | `X-API-Key`(仅 per-server `agent_api_key`;经 `POST /api/servers/{id}/agent-key` 生成,BL-06 起无 global 回退) |
|
||||
| WebSocket | JWT query param |
|
||||
| 安装向导 | 无 JWT(仅安装模式可用) |
|
||||
|
||||
## 登录防暴破
|
||||
|
||||
- `auth_service.py`:失败计数 → 15min 锁定 → HTTP 429
|
||||
- 刷新令牌重用检测:旧 refresh 作废、`token_version` 递增
|
||||
|
||||
## 密钥(禁改 / 禁提交)
|
||||
|
||||
- `.env` 永不入 git;`SECRET_KEY` / `API_KEY` / `ENCRYPTION_KEY` / `DATABASE_URL` 不可从 settings 表覆盖
|
||||
- UI 不展示可改的 `API_KEY`
|
||||
|
||||
## 运维信任模型
|
||||
|
||||
- 鉴权管理员即信任用户,不做 RBAC
|
||||
- 必须防御:SQL/命令注入、XSS、凭据泄露
|
||||
- Shell:`shlex.quote()`;DELETE/UPDATE 须有 WHERE(MCP 同样)
|
||||
|
||||
## Cursor MySQL MCP
|
||||
|
||||
- 使用 `@yclenove/mysql-mcp-server`,**默认 `MYSQL_READONLY=true`**
|
||||
- `MYSQL_DATABASE_ALLOWLIST=nexus`(或实际库名)
|
||||
- 禁止在 MCP `env` 或 git 中提交生产密码;连接信息仅写在本地 `.env`
|
||||
- 查库优先 MCP `query` / `describe_table`,避免手写破坏性 SQL
|
||||
|
||||
## 审计
|
||||
|
||||
- 所有 CUD 写 `audit_logs`(含 `ip_address`)
|
||||
- WebSSH 会话与 `command_logs` 可追溯
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# Nexus 6.0 技术栈
|
||||
|
||||
## 架构分层(禁止跨层直连)
|
||||
|
||||
```
|
||||
web/app/*.html → server/api/*.py → application/services → infrastructure/* → domain/models
|
||||
```
|
||||
|
||||
- API 层:路由 + Pydantic 校验 + `Depends(get_current_admin)`,不写 SQL
|
||||
- Service 层:业务编排,通过 Repository 访问数据
|
||||
- Infrastructure:DB/Redis/SSH/Telegram 实现细节
|
||||
- Domain:SQLAlchemy ORM 模型,无 FastAPI 依赖
|
||||
|
||||
## 启动模式
|
||||
|
||||
- 无 `.env` → **安装模式**:仅 `/api/install/` + `/app/install.html`
|
||||
- 有 `.env` → 正常模式:`SECRET_KEY` / `API_KEY` / `ENCRYPTION_KEY` 缺一不可
|
||||
|
||||
## 配置三写(安装向导)
|
||||
|
||||
`web/data/config.php` + `.env`(`NEXUS_` 前缀)+ MySQL `settings` 表。
|
||||
**不可从 DB 覆盖**:`SECRET_KEY`、`API_KEY`、`ENCRYPTION_KEY`、`DATABASE_URL`。
|
||||
|
||||
## 数据流
|
||||
|
||||
Agent(60s) → Redis 实时 → 前端直读 → 10min → MySQL 历史。告警:WebSocket + Telegram。
|
||||
|
||||
## 后台任务(仅 primary worker)
|
||||
|
||||
`heartbeat_flush`(10min) · `self_monitor`(30s) · `schedule_runner`(60s) · `retry_runner`(5min)
|
||||
|
||||
## HTTP / DB 会话
|
||||
|
||||
- 普通 API:`DbSessionMiddleware` → `request.state.db`,禁止在 factory 里 `AsyncSessionLocal()` 后不 close
|
||||
- WebSocket / `/api/install/`:不走 DB 中间件
|
||||
|
||||
## 相关规则文件
|
||||
|
||||
- `nexus-python-backend.mdc` — `server/**/*.py`
|
||||
- `nexus-frontend.mdc` — `web/app/**`
|
||||
- `nexus-security.mdc` — 安全与凭据
|
||||
- `perfect-implementation.mdc` — 完美实现与文档纪律
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
description: 单 Agent 对话——禁止部门/Skill 分派,用户只与当前助手直接协作
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 单 Agent 对话(强制)
|
||||
|
||||
Nexus 只保留**你 ↔ 当前 Cursor 助手**这一条协作链。已删除:`docs/skills/`、`docs/team/`、`.cursor/skills/`;本机 `~/.claude/skills/` 中凡带 `group: 管理组|工程部|测试部|产品部|设计部` 的目录也已移除。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 扮演或「转交」管理组、工程部、测试部、产品部、设计部及项目经理/总监/`*-department` 等任何虚拟角色。
|
||||
- 未经用户明确要求时,**不要** `Read` 或按 `available_skills` 去加载 Skill 文件(含全局 `~/.claude/skills/`、`~/.cursor/skills-cursor/`)。
|
||||
- 用 Task/子代理模拟「开会、分派、部门审批」;子代理仅用于用户明确要求的并行搜代码/跑命令等**技术任务**,且不得冠部门名。
|
||||
- 在回复里写「已转交 XX 部」「请以产品经理身份」等话术。
|
||||
|
||||
## 允许
|
||||
|
||||
- 直接:澄清需求 → 设计/技术文档(`docs/design/`)→ 改代码 → 跑 `tests/` / 门控 → changelog。
|
||||
- 依据:`standards/`、`.cursor/rules/`(含 `perfect-implementation`、`nexus-*`、`audit-line-review`)。
|
||||
|
||||
## 测试(强制)
|
||||
|
||||
- **所有测试与「是否修好」由本 Agent 执行并留证据**(命令、生产 URL、浏览器、`docs/reports/*-verification.md`)。
|
||||
- **不得**把未验证项推给用户代测;用户只做 Agent 交付后的**终验**(见 `docs/reports/2026-06-01-final-acceptance-checklist.md`)。
|
||||
|
||||
## 默认协作
|
||||
|
||||
用户说什么就做什么;需要方案时在同一对话里说明取舍,不引入额外「智能体」层。
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
description: 完美实现原则 — 禁止降级/妥协/技术债;设计文档与技术文档先行;每次修改写 changelog
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 完美实现与文档纪律
|
||||
|
||||
## 实现标准(强制)
|
||||
|
||||
**所有设计和实现都必须是完美实现。**
|
||||
|
||||
- ❌ **不允许为了实现而实现** — 每一行代码、每一个接口必须有明确目的,达到项目既定的安全、架构与质量标准
|
||||
- ❌ **不允许降级实现** — 不得以「先用着」「以后再改」为由降低安全、性能、可维护性或用户体验
|
||||
- ❌ **不允许妥协实现** — 发现问题必须根治,不得用绕过、静默吞错、硬编码掩盖代替修复
|
||||
- ❌ **不允许留技术债** — 不得留下 TODO/FIXME/「临时方案」延后处理;当场能修必须当场修
|
||||
- ✅ **无法完美实现时必须停下** — 与用户沟通并确认替代方案后,才能换方案;不得擅自从权或自行降级
|
||||
|
||||
## 文档纪律(强制)
|
||||
|
||||
### 设计文档
|
||||
|
||||
- 所有**新功能、架构变更、非平凡重构**必须先有设计文档,再写代码
|
||||
- 设计文档路径:`docs/design/specs/YYYY-MM-DD-<topic>-design.md`
|
||||
- 设计文档至少包含:背景与目标、方案对比、选定方案及理由、接口/数据模型、安全与性能约束、验收标准
|
||||
|
||||
### 技术文档(开发前)
|
||||
|
||||
- 开发前输出技术文档(实施说明),路径:`docs/design/plans/YYYY-MM-DD-<topic>.md` 或 `docs/reports/<step>/engineering-report.md`
|
||||
- 技术文档至少包含:涉及文件清单、实现步骤、依赖与配置变更、测试要点、回滚方式
|
||||
- 项目里**已有模块但缺文档的,在改动时一并补全**,不得只改代码不留档
|
||||
|
||||
### Changelog(每次修改)
|
||||
|
||||
- **每一次**有意义的代码或配置修改,必须在 `docs/changelog/` 新增或更新记录
|
||||
- 命名:`YYYY-MM-DD-<简短主题>.md`
|
||||
- 每条记录至少包含:日期、变更摘要、动机、涉及文件、是否需迁移/重启、验证方式
|
||||
- 禁止把 changelog 写在 commit message 里代替项目文档
|
||||
|
||||
## 执行顺序
|
||||
|
||||
```
|
||||
需求确认 → 设计文档 → 技术文档 → 实现 → 测试验证 → changelog →(必要时)更新 CLAUDE.md/AGENTS.md
|
||||
```
|
||||
|
||||
## 禁止示例
|
||||
|
||||
```python
|
||||
# ❌ 技术债
|
||||
# TODO: fix later
|
||||
pass # workaround
|
||||
|
||||
# ❌ 降级
|
||||
except Exception:
|
||||
return None # 静默失败
|
||||
|
||||
# ❌ 无文档直接大改
|
||||
# (新增整模块却无任何 docs/design 或 docs/changelog 记录)
|
||||
```
|
||||
|
||||
## 允许的唯一例外
|
||||
|
||||
经用户**明确书面确认**后,方可采用非完美方案;须在 changelog 中记录例外原因、影响范围与后续补齐计划。
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"plugins": {
|
||||
"compound-engineering": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# Nexus 6.0 — Cursor 速查
|
||||
|
||||
> SSOT: [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md)
|
||||
> Handoff: [docs/project/AI-HANDOFF-2026-06-03.md](docs/project/AI-HANDOFF-2026-06-03.md)
|
||||
|
||||
## 栈
|
||||
|
||||
FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA(15 页)· Vite → `web/app/`
|
||||
|
||||
## 环境(勿在仓库写密码)
|
||||
|
||||
- 生产: `https://api.synaglobal.vip` · 端口 8600 · `ssh nexus`
|
||||
- 本地: `~/Nexus` 或 `$NEXUS_ROOT` · 见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)
|
||||
- 凭据: 仅 `.env`(不入 git)
|
||||
- **Gitea**:clone/pull 可匿名;**push** 用 `bash scripts/git-push.sh`(凭据在 `deploy/nexus-1panel.secrets.sh`,已 gitignore,勿提交)
|
||||
|
||||
## 关键路径
|
||||
|
||||
| 用途 | 路径 |
|
||||
|------|------|
|
||||
| 后端入口 | `server/main.py` |
|
||||
| 前端路由 | `frontend/src/router/index.ts` |
|
||||
| API 客户端 | `frontend/src/api/index.ts` |
|
||||
| Sync | `server/api/sync_v2.py` |
|
||||
| 门控 | `deploy/pre_deploy_check.sh` |
|
||||
| 本地验证 | `bash scripts/local_verify.sh` |
|
||||
|
||||
## 铁律
|
||||
|
||||
见 `.cursor/rules/perfect-implementation.mdc` · `nexus-security.mdc` — 完美实现、无静默吞错、无明文密钥、CUD 审计。
|
||||
|
||||
## Agent Git / diff(省 token、防卡顿)
|
||||
|
||||
- **禁止**无路径过滤的全仓 `git diff` / `git diff HEAD`(`web/app/assets` 构建产物可导致数十万行输出)。
|
||||
- **优先**:`git diff --stat` 或 `git diff -- frontend/src server/api tests docs`。
|
||||
- **提交**:只 `git add` 源码与文档;`web/app/assets/*` 由 Docker 内 `vite build` 生成,勿为 diff 把海量 hash 文件纳入上下文。
|
||||
- **部署/门控**:定向 pytest(如 `tests/integration/test_*.py`),避免并行全仓 diff + 本地 build + 远程镜像重建。
|
||||
|
||||
## 部署
|
||||
|
||||
默认 **本机 rsync 直传**(不 push Gitea):
|
||||
|
||||
```bash
|
||||
bash deploy/pre_deploy_check.sh && bash deploy/deploy-production.sh
|
||||
```
|
||||
|
||||
仅同步源码(不重建镜像):`bash deploy/rsync-local-to-server.sh`
|
||||
仍走 Gitea 拉取:`NEXUS_DEPLOY_VIA_GIT=1 bash deploy/deploy-production.sh`
|
||||
|
||||
前端: `cd frontend && npx vite build` → 部署脚本内 tar/scp 到 `web/app/`
|
||||
|
||||
## 进度条(改代码时输出)
|
||||
|
||||
```
|
||||
□实现 □本地验证 □审计8步 □部署 □健康检查 □浏览器验证 □changelog
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
.git
|
||||
.gitignore
|
||||
.claude
|
||||
.cursor
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
**/.pytest_cache
|
||||
**/.mypy_cache
|
||||
**/.ruff_cache
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
node_modules
|
||||
|
||||
.env
|
||||
docker/.env
|
||||
docker/runtime
|
||||
|
||||
tests/
|
||||
docs/
|
||||
standards/
|
||||
*.md
|
||||
!docker/README.md
|
||||
|
||||
web/uploads
|
||||
backups/
|
||||
deploy/gate_log.jsonl
|
||||
|
||||
# Release/build local artifacts
|
||||
frontend-v2/node_modules
|
||||
.venv
|
||||
.venv-*
|
||||
venv
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
.playwright-mcp
|
||||
2025.2
|
||||
=2025.2
|
||||
*.bak-
|
||||
tmp/
|
||||
@@ -0,0 +1,16 @@
|
||||
# EditorConfig — keep LF for code deployed to Ubuntu
|
||||
# https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{bat,cmd}]
|
||||
end_of_line = crlf
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,28 @@
|
||||
# Line endings: Ubuntu production + bash scripts require LF in git.
|
||||
# Windows dev: set `git config core.autocrlf false` and rely on this file.
|
||||
|
||||
*.sh text eol=lf
|
||||
*.py text eol=lf
|
||||
*.vue text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.mts text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.css text eol=lf
|
||||
*.scss text eol=lf
|
||||
*.html text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.toml text eol=lf
|
||||
*.ini text eol=lf
|
||||
*.sql text eol=lf
|
||||
*.md text eol=lf
|
||||
*.txt text eol=lf
|
||||
.env.example text eol=lf
|
||||
.gitattributes text eol=lf
|
||||
.editorconfig text eol=lf
|
||||
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
@@ -35,18 +35,58 @@ jobs:
|
||||
run: mypy server/ --ignore-missing-imports
|
||||
continue-on-error: true # Type errors don't block deployment yet
|
||||
|
||||
- name: Run tests with coverage
|
||||
- name: Run fast tests with coverage
|
||||
run: |
|
||||
coverage run -m pytest tests/ -v --tb=short
|
||||
coverage run -m pytest tests/ -m "not chain and not slow" -q --tb=short
|
||||
coverage report --fail-under=50
|
||||
coverage xml
|
||||
|
||||
- name: Run chain tests
|
||||
run: pytest tests/chain -m chain -q --tb=short
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage.xml
|
||||
|
||||
# ── CI: E2E (main only, optional secrets) ──
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
working-directory: frontend
|
||||
run: npx playwright install chromium
|
||||
|
||||
- name: Run Playwright E2E
|
||||
working-directory: frontend
|
||||
env:
|
||||
NEXUS_E2E_BASE: ${{ secrets.NEXUS_E2E_BASE || 'http://127.0.0.1:8600' }}
|
||||
NEXUS_E2E_USER: ${{ secrets.NEXUS_E2E_USER || 'admin' }}
|
||||
NEXUS_E2E_PASSWORD: ${{ secrets.NEXUS_E2E_PASSWORD }}
|
||||
run: |
|
||||
if [ -z "$NEXUS_E2E_PASSWORD" ]; then
|
||||
echo "NEXUS_E2E_PASSWORD not set — skip E2E"
|
||||
exit 0
|
||||
fi
|
||||
npm run test:e2e
|
||||
continue-on-error: true
|
||||
|
||||
# ── CD: Deploy ──
|
||||
deploy:
|
||||
needs: test # Only deploy if tests pass
|
||||
|
||||
+89
-57
@@ -1,58 +1,90 @@
|
||||
# Nexus — Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.eggs/
|
||||
|
||||
# Environment (NEVER commit — contains production credentials)
|
||||
.env
|
||||
.install_locked
|
||||
.install_state.json
|
||||
SECRETS.md
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
nexus.log
|
||||
|
||||
# Node offline cache for WSL install (large tarball)
|
||||
.cache/
|
||||
|
||||
# Test
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
.coverage
|
||||
coverage.xml
|
||||
|
||||
# Node (for Tailwind/Alpine.js build)
|
||||
node_modules/
|
||||
web/css/tailwind-output.css
|
||||
|
||||
# Uploads
|
||||
web/uploads/
|
||||
|
||||
# Data (sensitive)
|
||||
web/data/config.php
|
||||
web/data/*.db
|
||||
|
||||
# Deploy
|
||||
deploy/*.key
|
||||
|
||||
# Backups
|
||||
# Nexus — Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
build/
|
||||
.eggs/
|
||||
|
||||
# Environment (NEVER commit — contains production credentials)
|
||||
.env
|
||||
.install_locked
|
||||
.install_state.json
|
||||
SECRETS.md
|
||||
.git-askpass-nexus.sh
|
||||
deploy/nexus-1panel.secrets.sh
|
||||
*.pem
|
||||
.env.*
|
||||
.venv/
|
||||
.venv-*/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Agent memory (local MCP persistence, do not commit)
|
||||
.claude/mcp-memory.jsonl
|
||||
|
||||
# MCP tool data (local development artifacts)
|
||||
.megamemory/
|
||||
.playwright-mcp/
|
||||
frontend/test-results/
|
||||
frontend/e2e/.auth/
|
||||
tmp/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
nexus.log
|
||||
|
||||
# Node offline cache for WSL install (large tarball)
|
||||
.cache/
|
||||
|
||||
# Test
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
.coverage
|
||||
coverage.xml
|
||||
|
||||
# Node (for Tailwind/Alpine.js build)
|
||||
node_modules/
|
||||
web/css/tailwind-output.css
|
||||
|
||||
# Build output (Vite/Vuetify frontend)
|
||||
# SPA 构建产物纳入 Git,便于服务器 git pull 部署(见 deploy/deploy-frontend.sh)
|
||||
# 仍忽略本地临时文件
|
||||
web/app/assets/.tmp/
|
||||
# Legacy dist dir (no longer used)
|
||||
**/dist/
|
||||
|
||||
# Uploads
|
||||
web/uploads/
|
||||
|
||||
# Data (sensitive)
|
||||
web/data/config.php
|
||||
web/data/*.db
|
||||
web/data/1panel-hosts.json
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
|
||||
# Docker local secrets
|
||||
docker/.env
|
||||
docker/.env.prod
|
||||
!docker/.env.example
|
||||
!docker/.env.prod.example
|
||||
scripts/npm-proxy.env
|
||||
|
||||
# Deploy
|
||||
deploy/*.key
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
@@ -1,169 +1,96 @@
|
||||
# Nexus 6.0 — Project Memory
|
||||
# Nexus 6.0 — Agent 入口(薄 stub)
|
||||
|
||||
## 项目概述
|
||||
Nexus 是一个 2000+ 服务器运维管理平台,从旧的 MultiSync (PHP + AdminLTE + pymysql) 完全重构为 Clean Architecture (FastAPI + Async SQLAlchemy + Redis + WebSocket + Telegram)。前端已全面迁移到 Tailwind CSS v4 + Alpine.js,PHP 依赖已移除。
|
||||
> **长文 SSOT**: [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md) + [附录](docs/project/nexus-functional-development-guide-appendix.md)
|
||||
> **会话接续**: [docs/project/AI-HANDOFF-2026-06-03.md](docs/project/AI-HANDOFF-2026-06-03.md)
|
||||
> **文档总索引**: [docs/README.md](docs/README.md)
|
||||
|
||||
## 仓库结构
|
||||
- **唯一仓库**: Nexus (Gitea, 仓库地址在 .env 中配置)
|
||||
- **不再使用** multi-server-sync 仓库(旧项目,仅保留历史)
|
||||
- **部署路径**: 由 `NEXUS_DEPLOY_PATH` 环境变量决定(安装向导自动写入 .env)
|
||||
- **域名/CORS**: 由 `NEXUS_CORS_ORIGINS` 环境变量决定(安装向导自动写入)
|
||||
## 项目概要
|
||||
|
||||
## 目录结构 (仓库根目录)
|
||||
```
|
||||
Nexus/
|
||||
├── server/ ← Python FastAPI后端 (Clean Architecture 4层)
|
||||
│ ├── api/ ← API路由 (含 install.py 安装向导API)
|
||||
│ ├── domain/ ← SQLAlchemy模型 (14张表)
|
||||
│ ├── infrastructure/← SSH连接池 + Redis + Telegram
|
||||
│ ├── background/ ← 后台任务 (心跳刷新/自检/调度/重试)
|
||||
│ └── config.py ← Pydantic Settings + MySQL覆盖
|
||||
├── web/app/ ← Tailwind+Alpine.js 纯静态前端 (12个HTML页面)
|
||||
│ ├── install.html ← 安装向导 (5步)
|
||||
│ ├── login.html ← JWT登录+TOTP双因素
|
||||
│ └── ... ← index/servers/files/push/scripts等
|
||||
├── deploy/ ← Supervisor配置 + Shell健康检查脚本
|
||||
├── docs/ ← 项目文档
|
||||
├── tests/ ← 测试
|
||||
├── .env ← NOT IN GIT (安装向导生成)
|
||||
└── requirements.txt ← Python依赖
|
||||
Nexus:2000+ 子机运维平台。后端 FastAPI + MySQL + Redis;前端 **Vue 3 + Vuetify 4 + TypeScript SPA**(15 页,`frontend/src/pages/`);构建输出 `web/app/`;安装向导 `web/app/install.html`。
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
docker compose up -d mysql redis
|
||||
bash scripts/start-dev.sh # API :8600
|
||||
bash scripts/local_verify.sh # L2b
|
||||
cd frontend && npm run dev # :3000/app/
|
||||
```
|
||||
|
||||
## 启动模式
|
||||
- **安装模式** (无 .env): 仅 `/api/install/` + 静态文件可用,其他路由返回503
|
||||
- **正常模式** (有 .env): 完整初始化 DB/Redis/后台任务
|
||||
路径见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)。
|
||||
|
||||
## 实现原则(强制)
|
||||
## Git(本机 + Gitea)
|
||||
|
||||
**所有设计和实现都必须是完美实现。** 详见 `.cursor/rules/perfect-implementation.mdc`。
|
||||
- 改完代码:**`git commit` 后 `bash scripts/git-push.sh` 推到 Gitea**(凭据 `deploy/nexus-1panel.secrets.sh`);大仓库可用 `scripts/git-push-axs-full.sh`。
|
||||
- 本地 commit 仍用于回滚;**不要漏推 Gitea**(用户约定 2026-07-09)。
|
||||
- `.env`、`SECRETS.md`、`*.pem`、`deploy/nexus-1panel.secrets.sh` 已在 `.gitignore`,**勿提交**。
|
||||
- 改代码仍写 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)。
|
||||
|
||||
| 禁止 | 要求 |
|
||||
|------|------|
|
||||
| 为实现而实现、降级、妥协、技术债(TODO/FIXME 延后) | 每一变更达到安全/架构/质量标准 |
|
||||
| 无法完美实现时擅自从权换方案 | 停下与用户沟通,确认后再换方案 |
|
||||
| 无文档直接开发 | 设计文档 → 技术文档 → 代码 |
|
||||
| 改代码不写 changelog | 每次修改记录到 `docs/changelog/YYYY-MM-DD-*.md` |
|
||||
## 强制约束
|
||||
|
||||
文档路径:设计 `docs/design/specs/` · 技术 `docs/design/plans/` 或 `docs/reports/` · 变更 `docs/changelog/`
|
||||
- 完美实现:`.cursor/rules/perfect-implementation.mdc`
|
||||
- 安全 / 后端 / 前端:`.cursor/rules/nexus-*.mdc`
|
||||
- 改代码必 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)
|
||||
- 测试由 Agent 完成;用户只做终验
|
||||
|
||||
## Cursor 规则与 MCP
|
||||
## 不可改
|
||||
|
||||
- 规则:`.cursor/rules/`(`perfect-implementation` + `nexus-*` 技术栈)
|
||||
- Skills:`.cursor/skills/`([superpowers-zh](https://github.com/jnMetaCode/superpowers-zh) 20 个,重装:`npx superpowers-zh --tool cursor`)
|
||||
- MySQL MCP:`docs/project/mysql-mcp-setup.md` · 配置后运行 `python scripts/sync_mysql_mcp_env.py`
|
||||
`API_KEY` · `SECRET_KEY` · `ENCRYPTION_KEY` · `DATABASE_URL`
|
||||
|
||||
## 已确认的关键决策
|
||||
## 部署
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
|------|------|------|
|
||||
| API_KEY | ❌ 禁改,UI不显示 | 加密一致性 + 子服务器认证 |
|
||||
| SECRET_KEY | ❌ 禁改 | 加密回退密钥 |
|
||||
| DATABASE_URL | ❌ 禁改 | 启动必需 |
|
||||
| system_name/title | ✅ 可改 | 前端显示 |
|
||||
| pool_size/overflow | ✅ 可改 | 安装向导自动推荐 |
|
||||
| redis_url | ✅ 可改 | Redis连接 |
|
||||
| 告警阈值 (CPU/mem/disk) | ✅ 可改 | 监控参数 (默认80%) |
|
||||
| Telegram配置 | ✅ 可改 | 告警推送 |
|
||||
| Agent心跳间隔 | 60s | 不太频繁 |
|
||||
| Redis→MySQL刷新 | 10min | 降低MySQL负载 |
|
||||
| 心跳数据流 | Agent→Redis→前端直读→10min→MySQL | 前端实时, MySQL只存历史 |
|
||||
| 告警推送 | WebSocket→前端 + Telegram | 即时感知 |
|
||||
| Python守护 | 3层: Supervisor + Python自检(30s) + Shell(cron 1min) | 全覆盖 |
|
||||
| 安装向导 | install.html + FastAPI API | D2: 替代install.php |
|
||||
| 配置源 | 安装向导→config.php(PHP兼容)+settings表(MySQL)+.env(Python) | 三写一致 |
|
||||
| Agent告警心跳 | CPU/内存>80% 或变化>10% 时主动上报 | 智能监控 |
|
||||
| 仓库统一 | 所有代码统一在Nexus仓库 | 不再用两个仓库 |
|
||||
默认 **本机 rsync → SSH 生产机**;**代码先 push Gitea 再部署**(见上节 Git):
|
||||
|
||||
## 实现状态 (Phase A+B+C+D+E) — ~99% 完成
|
||||
|
||||
### ✅ 已完成 (全部6步 + 技术债务 + UX增强)
|
||||
| 模块 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| **Step 0: 基础设施** | | Redis + WebSocket |
|
||||
| E1 WebSocket推送 | server/api/websocket.py | alert/recovery/system broadcast |
|
||||
| E2 心跳→Redis+告警 | server/api/agent.py | Redis write + threshold detection |
|
||||
| E3 Redis→MySQL刷新 | server/background/heartbeat_flush.py | 10min batch |
|
||||
| **Step 1: 数据层** | | 14张表 |
|
||||
| E4 load_settings_from_db | server/config.py | MySQL overrides mutable settings |
|
||||
| **Step 2: 认证层** | | JWT + TOTP |
|
||||
| **Step 3: Web SSH** | | asyncssh + xterm.js |
|
||||
| E5 asyncssh连接池 | server/infrastructure/ssh/asyncssh_pool.py | 引用计数模式 |
|
||||
| E6 WebSSH前端 | web/app/terminal.html | xterm.js + Koko协议 + 自动resize |
|
||||
| **Step 4: Sync引擎** | | 4种同步模式 |
|
||||
| **Step 5: 前端迁移** | | Tailwind+Alpine.js |
|
||||
| E7 13个前端页面 | web/app/*.html | 含install.html安装向导 |
|
||||
| E8 安装向导API | server/api/install.py | 6端点, 无JWT, 临时引擎 |
|
||||
| E9 条件启动模式 | server/main.py | 无.env=安装模式 |
|
||||
| **后台任务** | | |
|
||||
| E10 Python自检 | server/background/self_monitor.py | 30s loop |
|
||||
| E11 Telegram推送 | server/infrastructure/telegram/__init__.py | alert/recovery/system |
|
||||
| E12 /health端点 | server/api/health.py | Shell health_monitor.sh checks this |
|
||||
| **技术债务** | | |
|
||||
| D1 ✅ paramiko删除 | — | pool.py已删除, 全部迁移asyncssh |
|
||||
| D2 ✅ install迁移 | server/api/install.py | install.php→install.html+API |
|
||||
| D3 ✅ WebSSH前端 | web/app/terminal.html | xterm.js+Koko协议+全屏 |
|
||||
| **安全增强** | | |
|
||||
| S1 JWT全局保护 | server/api/*.py | 所有业务API都有JWT验证 |
|
||||
| S2 审计日志 | server/api/*.py | 所有CUD操作记录审计日志 |
|
||||
| S3 登录防暴破 | server/application/services/auth_service.py | 失败计数+15min锁定+429状态码 |
|
||||
| **前端UX增强** | | |
|
||||
| UX1 共享布局 | web/app/layout.js | 11页面统一侧边栏+全局搜索+用户信息 |
|
||||
| UX2 Toast通知 | web/app/api.js | 全局toast()函数,4种类型 |
|
||||
| UX3 全局搜索 | server/api/search.py | 跨服务器/脚本/凭据/调度搜索 |
|
||||
| UX4 服务器详情 | web/app/servers.html | 点击展开详情面板(系统信息/同步日志/SSH会话) |
|
||||
| UX5 脚本编辑 | web/app/scripts.html | 点击脚本名编辑+执行结果格式化 |
|
||||
| UX6 推送增强 | web/app/push.html | 逐服务器状态+进度条+推送历史 |
|
||||
| UX7 审计分页 | web/app/audit.html | 分页控件+操作过滤+每页50条 |
|
||||
| UX8 重试操作 | web/app/retries.html | 手动重试+删除按钮+状态过滤 |
|
||||
| UX9 文件浏览 | web/app/files.html | 面包屑导航+目录排序+父目录链接 |
|
||||
| UX10 登录安全 | web/app/login.html | 密码可见切换+失败计数+429锁定+shake动画 |
|
||||
| **P1功能补全** | | |
|
||||
| P1-1 资产管理 | web/app/assets.html | Platform/Node管理页面 |
|
||||
| P1-2 服务器表单 | web/app/servers.html | 补全platform/node/protocols字段 |
|
||||
| P1-3 Push对接 | web/app/push.html + sync_v2.py | target_path对接+统一SyncEngineV2 |
|
||||
| P1-4 安全设置 | web/app/settings.html | TOTP QR码+修改密码+API Key复制 |
|
||||
| P1-5 WebSSH授权 | server/api/webssh.py | SSH凭据验证+4003关闭码+审计 |
|
||||
| P1-6 Agent认证 | server/api/agent.py | 每服务器API Key + 全局回退 |
|
||||
| P1-7 Config去重 | server/application/services/sync_engine_v2.py | sed去重+备份+_rollback_config() |
|
||||
| **P2安全增强** | | |
|
||||
| P2-1 告警防抖 | server/api/websocket.py | 5分钟冷却+Telegram去重+恢复重置 |
|
||||
| P2-2 命令日志 | web/app/commands.html | 命令/会话双视图+服务器过滤+危险命令高亮 |
|
||||
| P2-3 DB分页 | server/infrastructure/database/server_repo.py | DB层offset/limit+count |
|
||||
| P2-4 Dashboard实时 | web/app/index.html | WS触发stats刷新+声音+标签页闪烁 |
|
||||
| P2-5 移动适配 | web/app/layout.js | <768px侧边栏overlay+backdrop+表格滚动 |
|
||||
| P2-6 会话安全 | server/api/auth_jwt.py + api.js | 8小时超时+JWT updated_at+刷新重用检测 |
|
||||
| P2-7 凭据加密 | server/api/servers.py | Server.password/ssh_key_private Fernet加密存储 |
|
||||
| P2-8 凭据脱敏 | server/api/scripts.py + servers.py | API响应不返回密码,password_set布尔标记 |
|
||||
| P2-9 DB备份 | deploy/db_backup.sh | mysqldump+30天保留+cron集成 |
|
||||
|
||||
### 🔄 待测试
|
||||
- T1: 心跳Redis写入验证
|
||||
- T2: WebSocket连接测试
|
||||
- T3: 3层守护测试 (kill Python→Supervisor重启)
|
||||
- T4: Telegram推送测试
|
||||
- T5: install.html 5步流程测试
|
||||
|
||||
### 测试流程 (用户确认)
|
||||
1. WSL本地基础测试 → 2. 推到仓库 → 3. 外网服务器正式测试
|
||||
|
||||
## 配置三写机制
|
||||
安装向导 Step 3 POST处理同时写入:
|
||||
1. `web/data/config.php` — PHP兼容配置 (API_BASE_URL, DB_*, API_KEY)
|
||||
2. `.env` — Python后端启动必需 (NEXUS_前缀)
|
||||
3. `settings` MySQL表 — 共享动态配置 (api_key, thresholds, Telegram等)
|
||||
|
||||
不可改项 (SECRET_KEY/API_KEY/ENCRYPTION_KEY/DATABASE_URL) 不在MySQL覆盖映射中。
|
||||
|
||||
## 3层守护机制
|
||||
- **Layer 1**: Supervisor — Python崩溃自动重启
|
||||
- **Layer 2**: Python self_monitor — 每30s检查Redis/MySQL/WebSocket
|
||||
- **Layer 3**: Shell health_monitor.sh — 每分钟HTTP GET /health, 连续3次失败→supervisorctl restart + Telegram
|
||||
|
||||
## 数据流
|
||||
```bash
|
||||
bash deploy/pre_deploy_check.sh
|
||||
bash scripts/git-push.sh # 或 git-push-axs-full.sh
|
||||
bash deploy/deploy-production.sh # rsync + Docker upgrade --skip-git + 前端
|
||||
```
|
||||
Agent心跳(60s) → Redis(实时) → 前端直读 → 10min批量 → MySQL(历史)
|
||||
告警: CPU/mem/disk > threshold → WebSocket推送浏览器 + Telegram推送手机
|
||||
恢复: 之前告警的指标恢复正常 → 自动推送恢复通知
|
||||
Redis心跳key: heartbeat:{server_id} (HSET, TTL=600s)
|
||||
Redis告警key: alerts:{server_id} (SET, TTL=3600s)
|
||||
|
||||
可选:`NEXUS_DEPLOY_VIA_GIT=1` 恢复远程 `git pull`。
|
||||
|
||||
详情见功能指南 §17–18。
|
||||
|
||||
## 归档文档
|
||||
|
||||
历史审计合并卷:`docs/archive/README.md`
|
||||
|
||||
## 代码地图(Agent 先读这里,勿全库搜)
|
||||
|
||||
```
|
||||
server/main.py # 路由挂载入口
|
||||
server/config.py # 配置(.env + settings 表)
|
||||
server/api/*.py # HTTP/WebSocket 路由(按模块拆分)
|
||||
server/application/ # 业务 services
|
||||
server/infrastructure/ # DB / Redis / SSH / Telegram
|
||||
server/domain/models/ # SQLAlchemy ORM
|
||||
frontend/src/pages/ # Vue 页面(15 页 SPA)
|
||||
frontend/src/router/ # Hash 路由 #/
|
||||
frontend/src/api/ # 前端 API 客户端
|
||||
frontend/e2e/ # Playwright 验收
|
||||
tests/test_*.py # pytest
|
||||
docs/project/ # 功能/部署 SSOT
|
||||
deploy/ # 生产部署脚本
|
||||
scripts/ # 本地验证、MCP、门控
|
||||
.cursor/mcp.json # mysql-mcp
|
||||
```
|
||||
|
||||
## 问题 → 文件(常见锚点)
|
||||
|
||||
| 问什么 | 先看 |
|
||||
|--------|------|
|
||||
| 某 API 怎么实现 | `server/main.py` 找 router → `server/api/<模块>.py` |
|
||||
| 服务器列表/搜索/exec | `server/api/servers.py`, `server/api/scripts.py` |
|
||||
| 认证/JWT | `server/api/auth.py`, `server/api/dependencies.py` |
|
||||
| Agent 心跳/安装 | `server/api/agent.py` |
|
||||
| WebSSH/终端 | `server/api/webssh.py`, `server/api/terminal.py` |
|
||||
| 同步/文件/搜索 | `server/api/sync_v2.py`, `files.py`, `search.py` |
|
||||
| 设置/审计/告警 | `server/api/settings.py` |
|
||||
| 前端某页 UI | `frontend/src/router/index.ts` → `frontend/src/pages/` |
|
||||
| MySQL MCP | `.cursor/mcp.json`, `scripts/linux_mcp_mysql.sh` |
|
||||
| 本地开发路径 | `docs/project/linux-dev-paths.md` |
|
||||
| 14 页/API 全览 | `docs/project/nexus-functional-development-guide.md` §9–10 |
|
||||
|
||||
## 探索规则(省 token)
|
||||
|
||||
详见 `.cursor/rules/agent-exploration.mdc`。要点:**先地图再 Grep(带 path)→ Read ≤5 个文件 → 停止**。
|
||||
|
||||
@@ -1,167 +1,28 @@
|
||||
# Nexus 6.0 — Project Memory
|
||||
# Nexus 6.0 — Agent 入口(薄 stub)
|
||||
|
||||
## 项目概述
|
||||
Nexus 是一个 2000+ 服务器运维管理平台,从旧的 MultiSync (PHP + AdminLTE + pymysql) 完全重构为 Clean Architecture (FastAPI + Async SQLAlchemy + Redis + WebSocket + Telegram)。前端已全面迁移到 Tailwind CSS v4 + Alpine.js,PHP 依赖已移除。
|
||||
> 与 [AGENTS.md](AGENTS.md) 相同入口;长文见 [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md)。
|
||||
|
||||
## 仓库结构
|
||||
- **唯一仓库**: Nexus (Gitea, 仓库地址在 .env 中配置)
|
||||
- **不再使用** multi-server-sync 仓库(旧项目,仅保留历史)
|
||||
- **部署路径**: 由 `NEXUS_DEPLOY_PATH` 环境变量决定(安装向导自动写入 .env)
|
||||
- **域名/CORS**: 由 `NEXUS_CORS_ORIGINS` 环境变量决定(安装向导自动写入)
|
||||
## 快速链接
|
||||
|
||||
| 用途 | 路径 |
|
||||
|------|------|
|
||||
| 功能 / API / 14 页 | `docs/project/nexus-functional-development-guide.md` |
|
||||
| 附录(sync 端点、调用链) | `docs/project/nexus-functional-development-guide-appendix.md` |
|
||||
| 当前 handoff | `docs/project/AI-HANDOFF-2026-06-03.md` |
|
||||
| 本地验证 | `bash scripts/local_verify.sh` |
|
||||
| 验收 L0–L5 | `docs/project/development-acceptance-standard.md` |
|
||||
| 风险接受(单人运维) | `docs/project/risk-acceptance-single-operator.md` |
|
||||
| 文档索引 | `docs/README.md` |
|
||||
| 历史归档 | `docs/archive/README.md` |
|
||||
|
||||
## 栈
|
||||
|
||||
FastAPI · Vue 3 SPA · Hash 路由 `#/` · 门控 `deploy/pre_deploy_check.sh`(7 道)
|
||||
|
||||
## 流程
|
||||
|
||||
## 目录结构 (仓库根目录)
|
||||
```
|
||||
Nexus/
|
||||
├── server/ ← Python FastAPI后端 (Clean Architecture 4层)
|
||||
│ ├── api/ ← API路由 (含 install.py 安装向导API)
|
||||
│ ├── domain/ ← SQLAlchemy模型 (14张表)
|
||||
│ ├── infrastructure/← SSH连接池 + Redis + Telegram
|
||||
│ ├── background/ ← 后台任务 (心跳刷新/自检/调度/重试)
|
||||
│ └── config.py ← Pydantic Settings + MySQL覆盖
|
||||
├── web/app/ ← Tailwind+Alpine.js 纯静态前端 (12个HTML页面)
|
||||
│ ├── install.html ← 安装向导 (5步)
|
||||
│ ├── login.html ← JWT登录+TOTP双因素
|
||||
│ └── ... ← index/servers/files/push/scripts等
|
||||
├── deploy/ ← Supervisor配置 + Shell健康检查脚本
|
||||
├── docs/ ← 项目文档
|
||||
├── tests/ ← 测试
|
||||
├── .env ← NOT IN GIT (安装向导生成)
|
||||
└── requirements.txt ← Python依赖
|
||||
实现 → local_verify → 审计(按 diff 量级)→ changelog →(批准)部署
|
||||
```
|
||||
|
||||
## 启动模式
|
||||
- **安装模式** (无 .env): 仅 `/api/install/` + 静态文件可用,其他路由返回503
|
||||
- **正常模式** (有 .env): 完整初始化 DB/Redis/后台任务
|
||||
|
||||
## 已确认的关键决策
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
|------|------|------|
|
||||
| API_KEY | ❌ 禁改,UI不显示 | 加密一致性 + 子服务器认证 |
|
||||
| SECRET_KEY | ❌ 禁改 | 加密回退密钥 |
|
||||
| DATABASE_URL | ❌ 禁改 | 启动必需 |
|
||||
| system_name/title | ✅ 可改 | 前端显示 |
|
||||
| pool_size/overflow | ✅ 可改 | 安装向导自动推荐 |
|
||||
| redis_url | ✅ 可改 | Redis连接 |
|
||||
| 告警阈值 (CPU/mem/disk) | ✅ 可改 | 监控参数 (默认80%) |
|
||||
| Telegram配置 | ✅ 可改 | 告警推送 |
|
||||
| Agent心跳间隔 | 60s | 不太频繁 |
|
||||
| Redis→MySQL刷新 | 10min | 降低MySQL负载 |
|
||||
| 心跳数据流 | Agent→Redis→前端直读→10min→MySQL | 前端实时, MySQL只存历史 |
|
||||
| 告警推送 | WebSocket→前端 + Telegram | 即时感知 |
|
||||
| Python守护 | 3层: Supervisor + Python自检(30s) + Shell(cron 1min) | 全覆盖 |
|
||||
| 安装向导 | install.html + FastAPI API | D2: 替代install.php |
|
||||
| 配置源 | 安装向导→config.php(PHP兼容)+settings表(MySQL)+.env(Python) | 三写一致 |
|
||||
| Agent告警心跳 | CPU/内存>80% 或变化>10% 时主动上报 | 智能监控 |
|
||||
| 仓库统一 | 所有代码统一在Nexus仓库 | 不再用两个仓库 |
|
||||
|
||||
## 实现状态 (Phase A+B+C+D+E) — ~99% 完成
|
||||
|
||||
### ✅ 已完成 (全部6步 + 技术债务 + UX增强)
|
||||
| 模块 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| **Step 0: 基础设施** | | Redis + WebSocket |
|
||||
| E1 WebSocket推送 | server/api/websocket.py | alert/recovery/system broadcast |
|
||||
| E2 心跳→Redis+告警 | server/api/agent.py | Redis write + threshold detection |
|
||||
| E3 Redis→MySQL刷新 | server/background/heartbeat_flush.py | 10min batch |
|
||||
| **Step 1: 数据层** | | 14张表 |
|
||||
| E4 load_settings_from_db | server/config.py | MySQL overrides mutable settings |
|
||||
| **Step 2: 认证层** | | JWT + TOTP |
|
||||
| **Step 3: Web SSH** | | asyncssh + xterm.js |
|
||||
| E5 asyncssh连接池 | server/infrastructure/ssh/asyncssh_pool.py | 引用计数模式 |
|
||||
| E6 WebSSH前端 | web/app/terminal.html | xterm.js + Koko协议 + 自动resize |
|
||||
| **Step 4: Sync引擎** | | 4种同步模式 |
|
||||
| **Step 5: 前端迁移** | | Tailwind+Alpine.js |
|
||||
| E7 13个前端页面 | web/app/*.html | 含install.html安装向导 |
|
||||
| E8 安装向导API | server/api/install.py | 6端点, 无JWT, 临时引擎 |
|
||||
| E9 条件启动模式 | server/main.py | 无.env=安装模式 |
|
||||
| **后台任务** | | |
|
||||
| E10 Python自检 | server/background/self_monitor.py | 30s loop |
|
||||
| E11 Telegram推送 | server/infrastructure/telegram/__init__.py | alert/recovery/system |
|
||||
| E12 /health端点 | server/api/health.py | Shell health_monitor.sh checks this |
|
||||
| **技术债务** | | |
|
||||
| D1 ✅ paramiko删除 | — | pool.py已删除, 全部迁移asyncssh |
|
||||
| D2 ✅ install迁移 | server/api/install.py | install.php→install.html+API |
|
||||
| D3 ✅ WebSSH前端 | web/app/terminal.html | xterm.js+Koko协议+全屏 |
|
||||
| **安全增强** | | |
|
||||
| S1 JWT全局保护 | server/api/*.py | 所有业务API都有JWT验证 |
|
||||
| S2 审计日志 | server/api/*.py | 所有CUD操作记录审计日志 |
|
||||
| S3 登录防暴破 | server/application/services/auth_service.py | 失败计数+15min锁定+429状态码 |
|
||||
| **前端UX增强** | | |
|
||||
| UX1 共享布局 | web/app/layout.js | 11页面统一侧边栏+全局搜索+用户信息 |
|
||||
| UX2 Toast通知 | web/app/api.js | 全局toast()函数,4种类型 |
|
||||
| UX3 全局搜索 | server/api/search.py | 跨服务器/脚本/凭据/调度搜索 |
|
||||
| UX4 服务器详情 | web/app/servers.html | 点击展开详情面板(系统信息/同步日志/SSH会话) |
|
||||
| UX5 脚本编辑 | web/app/scripts.html | 点击脚本名编辑+执行结果格式化 |
|
||||
| UX6 推送增强 | web/app/push.html | 逐服务器状态+进度条+推送历史 |
|
||||
| UX7 审计分页 | web/app/audit.html | 分页控件+操作过滤+每页50条 |
|
||||
| UX8 重试操作 | web/app/retries.html | 手动重试+删除按钮+状态过滤 |
|
||||
| UX9 文件浏览 | web/app/files.html | 面包屑导航+目录排序+父目录链接 |
|
||||
| UX10 登录安全 | web/app/login.html | 密码可见切换+失败计数+429锁定+shake动画 |
|
||||
| **P1功能补全** | | |
|
||||
| P1-1 资产管理 | web/app/assets.html | Platform/Node管理页面 |
|
||||
| P1-2 服务器表单 | web/app/servers.html | 补全platform/node/protocols字段 |
|
||||
| P1-3 Push对接 | web/app/push.html + sync_v2.py | target_path对接+统一SyncEngineV2 |
|
||||
| P1-4 安全设置 | web/app/settings.html | TOTP QR码+修改密码+API Key复制 |
|
||||
| P1-5 WebSSH授权 | server/api/webssh.py | SSH凭据验证+4003关闭码+审计 |
|
||||
| P1-6 Agent认证 | server/api/agent.py | 每服务器API Key + 全局回退 |
|
||||
| P1-7 Config去重 | server/application/services/sync_engine_v2.py | sed去重+备份+_rollback_config() |
|
||||
| **P2安全增强** | | |
|
||||
| P2-1 告警防抖 | server/api/websocket.py | 5分钟冷却+Telegram去重+恢复重置 |
|
||||
| P2-2 命令日志 | web/app/commands.html | 命令/会话双视图+服务器过滤+危险命令高亮 |
|
||||
| P2-3 DB分页 | server/infrastructure/database/server_repo.py | DB层offset/limit+count |
|
||||
| P2-4 Dashboard实时 | web/app/index.html | WS触发stats刷新+声音+标签页闪烁 |
|
||||
| P2-5 移动适配 | web/app/layout.js | <768px侧边栏overlay+backdrop+表格滚动 |
|
||||
| P2-6 会话安全 | server/api/auth_jwt.py + api.js | 8小时超时+JWT updated_at+刷新重用检测 |
|
||||
| P2-7 凭据加密 | server/api/servers.py | Server.password/ssh_key_private Fernet加密存储 |
|
||||
| P2-8 凭据脱敏 | server/api/scripts.py + servers.py | API响应不返回密码,password_set布尔标记 |
|
||||
| P2-9 DB备份 | deploy/db_backup.sh | mysqldump+30天保留+cron集成 |
|
||||
|
||||
### 🔄 待测试
|
||||
- T1: 心跳Redis写入验证
|
||||
- T2: WebSocket连接测试
|
||||
- T3: 3层守护测试 (kill Python→Supervisor重启)
|
||||
- T4: Telegram推送测试
|
||||
- T5: install.html 5步流程测试
|
||||
|
||||
### 测试流程 (用户确认)
|
||||
1. WSL本地基础测试 → 2. 推到仓库 → 3. 外网服务器正式测试
|
||||
|
||||
## 配置三写机制
|
||||
安装向导 Step 3 POST处理同时写入:
|
||||
1. `web/data/config.php` — PHP兼容配置 (API_BASE_URL, DB_*, API_KEY)
|
||||
2. `.env` — Python后端启动必需 (NEXUS_前缀)
|
||||
3. `settings` MySQL表 — 共享动态配置 (api_key, thresholds, Telegram等)
|
||||
|
||||
不可改项 (SECRET_KEY/API_KEY/ENCRYPTION_KEY/DATABASE_URL) 不在MySQL覆盖映射中。
|
||||
|
||||
## 3层守护机制
|
||||
- **Layer 1**: Supervisor — Python崩溃自动重启
|
||||
- **Layer 2**: Python self_monitor — 每30s检查Redis/MySQL/WebSocket
|
||||
- **Layer 3**: Shell health_monitor.sh — 每分钟HTTP GET /health, 连续3次失败→supervisorctl restart + Telegram
|
||||
|
||||
## 实现原则(强制 — 见 `.cursor/rules/perfect-implementation.mdc`)
|
||||
|
||||
**所有设计和实现都必须是完美实现。**
|
||||
|
||||
- ❌ 不允许为了实现而实现 — 每一行代码必须有明确的目的和质量标准
|
||||
- ❌ 不允许降级实现 — 不得以「先用着」为由降低安全、架构或代码质量标准
|
||||
- ❌ 不允许妥协实现 — 不得在发现问题后选择绕过而非根治
|
||||
- ❌ 不允许留技术债 — 发现问题必须当场修复,不得标注 TODO/FIXME 延后
|
||||
- ✅ 无法完美实现时必须停下来与用户交流后再换方案,不得擅自从权
|
||||
|
||||
**文档纪律:**
|
||||
|
||||
- 新功能/架构变更:先写 `docs/design/specs/YYYY-MM-DD-*-design.md`,再写代码
|
||||
- 开发前:写 `docs/design/plans/` 或 `docs/reports/` 技术文档(文件清单、步骤、测试、回滚)
|
||||
- 项目缺文档的模块:改动时一并补全
|
||||
- 每次修改:在 `docs/changelog/YYYY-MM-DD-*.md` 留 changelog
|
||||
|
||||
## 数据流
|
||||
```
|
||||
Agent心跳(60s) → Redis(实时) → 前端直读 → 10min批量 → MySQL(历史)
|
||||
告警: CPU/mem/disk > threshold → WebSocket推送浏览器 + Telegram推送手机
|
||||
恢复: 之前告警的指标恢复正常 → 自动推送恢复通知
|
||||
Redis心跳key: heartbeat:{server_id} (HSET, TTL=600s)
|
||||
Redis告警key: alerts:{server_id} (SET, TTL=3600s)
|
||||
```
|
||||
强制规则:`.cursor/rules/` · `standards/`
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Nexus 6.0 — API + 静态 SPA (web/app)
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
openssh-client \
|
||||
rsync \
|
||||
sshpass \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server/ ./server/
|
||||
COPY web/app/ ./web/app/
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
RUN chmod +x ./docker/entrypoint.sh
|
||||
|
||||
ENV PYTHONPATH=/app \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
NEXUS_HOST=0.0.0.0 \
|
||||
NEXUS_PORT=8600
|
||||
|
||||
EXPOSE 8600
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||||
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
||||
@@ -0,0 +1,76 @@
|
||||
# Nexus 6.0 — production image (frontend build + API)
|
||||
# Build: docker compose -f docker/docker-compose.prod.yml build
|
||||
# See: docs/design/plans/2026-06-04-1panel-docker-production.md
|
||||
|
||||
# ── Stage 1: Vue/Vite SPA ──
|
||||
FROM node:20-bookworm-slim AS frontend
|
||||
WORKDIR /build/frontend
|
||||
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY frontend/ ./
|
||||
RUN npx vite build
|
||||
|
||||
# ── Stage 1b: React/Vite App V2 ──
|
||||
FROM node:20-bookworm-slim AS frontend_v2
|
||||
WORKDIR /build/frontend-v2
|
||||
|
||||
COPY frontend-v2/package.json frontend-v2/package-lock.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY frontend-v2/ ./
|
||||
RUN npm run build:safe
|
||||
|
||||
# ── Stage 2: Python API + built static assets ──
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
openssh-client \
|
||||
rsync \
|
||||
sshpass \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY server/ ./server/
|
||||
COPY web/agent/ ./web/agent/
|
||||
COPY --from=frontend_v2 /build/web/app-v2 ./web/app-v2/
|
||||
COPY --from=frontend /build/web/app ./web/app/
|
||||
# Login wallpaper cache (Vite does not emit this dir; sync also writes here at runtime)
|
||||
COPY web/app/wallpapers/ ./web/app/wallpapers/
|
||||
# Install wizard + static SPA assets not emitted by Vite
|
||||
COPY web/app/install.html ./web/app/install.html
|
||||
COPY web/app/servers_import_template.csv ./web/app/servers_import_template.csv
|
||||
RUN mkdir -p ./web/app/vendor
|
||||
COPY web/app/vendor/tailwindcss-browser.js \
|
||||
web/app/vendor/theme-init.js \
|
||||
web/app/vendor/theme.css \
|
||||
web/app/vendor/alpinejs.min.js \
|
||||
./web/app/vendor/
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
RUN chmod +x ./docker/entrypoint.sh \
|
||||
&& test -f /app/web/app/install.html \
|
||||
&& test -f /app/web/app-v2/index.html \
|
||||
&& test -f /app/web/app/vendor/alpinejs.min.js \
|
||||
&& test -f /app/web/app/vendor/tailwindcss-browser.js \
|
||||
&& test -f /app/web/app/vendor/theme.css \
|
||||
&& test -f /app/web/app/vendor/theme-init.js
|
||||
|
||||
ENV PYTHONPATH=/app \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
NEXUS_HOST=0.0.0.0 \
|
||||
NEXUS_PORT=8600
|
||||
|
||||
EXPOSE 8600
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||||
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
||||
+7
-313
@@ -1,317 +1,11 @@
|
||||
# Nexus 6.0 项目交接文档
|
||||
# Nexus 6.0 项目交接(已 supersede)
|
||||
|
||||
## 项目概述
|
||||
请以以下文档为准:
|
||||
|
||||
**项目名称**: Nexus 6.0
|
||||
**类型**: 2000+ 服务器运维管理平台
|
||||
**架构**: Clean Architecture (FastAPI + Async SQLAlchemy + Redis + WebSocket + Telegram)
|
||||
**前端**: Tailwind CSS v4 + Alpine.js (15个HTML页面)
|
||||
**状态**: ✅ 开发完成,待部署测试
|
||||
|
||||
---
|
||||
|
||||
## 仓库信息
|
||||
|
||||
- **仓库地址**: Gitea (地址在 .env 中配置)
|
||||
- **当前工作目录**: `C:\Users\uzuma\Desktop\svn\Nexus\.claude\worktrees\suspicious-williams-fe9492`
|
||||
- **Git分支**: 当前在 worktree 分支
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
| 文档 | 路径 |
|
||||
|------|------|
|
||||
| 后端框架 | FastAPI (Python 3.9+) |
|
||||
| 数据库 | MySQL 8.0 + Async SQLAlchemy 2.0 |
|
||||
| 缓存 | Redis |
|
||||
| 实时通信 | WebSocket |
|
||||
| SSH连接 | asyncssh (连接池模式) |
|
||||
| 告警推送 | Telegram Bot API |
|
||||
| 前端 | Tailwind CSS v4 + Alpine.js |
|
||||
| 进程守护 | Supervisor + Python自检 + Shell健康检查 |
|
||||
| 功能开发 SSOT | [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md) |
|
||||
| 会话接续 | [docs/project/AI-HANDOFF-2026-06-03.md](docs/project/AI-HANDOFF-2026-06-03.md) |
|
||||
| 文档总索引 | [docs/README.md](docs/README.md) |
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
Nexus/
|
||||
├── server/ # Python FastAPI 后端
|
||||
│ ├── api/ # API路由层 (15个模块)
|
||||
│ │ ├── agent.py # Agent心跳接收
|
||||
│ │ ├── assets.py # Platform/Node资产管理
|
||||
│ │ ├── auth.py # JWT登录/TOTP
|
||||
│ │ ├── auth_jwt.py # JWT验证中间件
|
||||
│ │ ├── files.py # 文件浏览
|
||||
│ │ ├── install.py # 安装向导API
|
||||
│ │ ├── push.py # 推送调度
|
||||
│ │ ├── scripts.py # 脚本库管理
|
||||
│ │ ├── servers.py # 服务器CRUD
|
||||
│ │ ├── settings.py # 系统设置
|
||||
│ │ ├── sync_v2.py # 同步引擎v2 (4种模式)
|
||||
│ │ ├── webssh.py # WebSSH终端
|
||||
│ │ └── websocket.py # WebSocket告警推送
|
||||
│ ├── application/ # 应用服务层
|
||||
│ │ └── services/ # 业务逻辑 (SyncEngineV2等)
|
||||
│ ├── domain/ # 领域层
|
||||
│ │ └── models/ # SQLAlchemy ORM (14张表)
|
||||
│ ├── infrastructure/ # 基础设施层
|
||||
│ │ ├── database/ # 数据库访问 (Repository模式)
|
||||
│ │ ├── redis/ # Redis客户端
|
||||
│ │ ├── ssh/ # SSH连接池 (asyncssh)
|
||||
│ │ └── telegram/ # Telegram推送
|
||||
│ ├── background/ # 后台任务
|
||||
│ │ ├── self_monitor.py # Python自检 (30s)
|
||||
│ │ ├── heartbeat_flush.py # Redis→MySQL刷新 (10min)
|
||||
│ │ ├── scheduler.py # 定时推送调度
|
||||
│ │ └── retry_worker.py # 失败重试队列
|
||||
│ └── config.py # Pydantic Settings
|
||||
├── web/app/ # 前端页面 (15个HTML)
|
||||
│ ├── install.html # 安装向导 (5步)
|
||||
│ ├── login.html # JWT登录+TOTP
|
||||
│ ├── index.html # 仪表盘
|
||||
│ ├── servers.html # 服务器管理
|
||||
│ ├── assets.html # Platform/Node管理
|
||||
│ ├── push.html # 文件推送
|
||||
│ ├── scripts.html # 脚本库
|
||||
│ ├── commands.html # 命令日志查询
|
||||
│ ├── files.html # 远程文件浏览
|
||||
│ ├── settings.html # 系统设置
|
||||
│ ├── audit.html # 审计日志
|
||||
│ ├── retries.html # 重试队列
|
||||
│ ├── terminal.html # WebSSH终端
|
||||
│ ├── api.js # 共享API模块 (JWT+Toast)
|
||||
│ └── layout.js # 共享布局组件
|
||||
├── deploy/ # 部署配置
|
||||
│ ├── supervisor/ # Supervisor配置
|
||||
│ ├── nginx/ # Nginx配置
|
||||
│ └── db_backup.sh # 数据库备份脚本
|
||||
├── docs/ # 项目文档
|
||||
├── requirements.txt # Python依赖
|
||||
└── test_p1_p2.py # 验证测试脚本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库模型 (14张表)
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `admins` | 管理员用户 (JWT+TOTP支持) |
|
||||
| `servers` | 服务器资产 (核心表) |
|
||||
| `platforms` | 平台类型模板 |
|
||||
| `nodes` | 资产节点树 |
|
||||
| `sync_logs` | 同步操作日志 |
|
||||
| `push_schedules` | 定时推送调度 |
|
||||
| `push_retry_jobs` | 失败重试队列 |
|
||||
| `audit_logs` | 审计日志 |
|
||||
| `scripts` | 脚本库 |
|
||||
| `script_executions` | 脚本执行记录 |
|
||||
| `password_presets` | SSH密码预设 |
|
||||
| `db_credentials` | 数据库凭据 |
|
||||
| `ssh_sessions` | WebSSH会话 |
|
||||
| `command_logs` | SSH命令日志 |
|
||||
| `login_attempts` | 登录防暴破记录 |
|
||||
| `settings` | 系统配置键值对 |
|
||||
|
||||
---
|
||||
|
||||
## 核心功能清单
|
||||
|
||||
### P0 核心功能 (已完成)
|
||||
- [x] JWT认证 + TOTP双因素
|
||||
- [x] 登录防暴破 (15分钟锁定)
|
||||
- [x] Agent心跳上报 (Redis实时)
|
||||
- [x] WebSocket实时告警推送
|
||||
- [x] Telegram告警通知
|
||||
- [x] 安装向导 (5步)
|
||||
|
||||
### P1 功能 (已完成)
|
||||
- [x] Platform/Node资产管理
|
||||
- [x] 服务器详情面板 (点击展开)
|
||||
- [x] WebSSH终端 (xterm.js)
|
||||
- [x] 推送页面增强 (逐服务器状态+进度条)
|
||||
- [x] 脚本编辑+执行结果格式化
|
||||
- [x] 全局Toast通知系统
|
||||
- [x] 审计日志分页+过滤
|
||||
- [x] 重试队列手动操作
|
||||
- [x] 文件浏览 (面包屑导航)
|
||||
|
||||
### P2 功能 (已完成)
|
||||
- [x] 告警聚合防抖 (5分钟冷却)
|
||||
- [x] 命令日志查询页面
|
||||
- [x] 服务器列表DB级分页
|
||||
- [x] Dashboard WebSocket实时刷新
|
||||
- [x] 移动端响应式适配
|
||||
- [x] 会话安全加固 (8小时超时)
|
||||
- [x] 服务器凭据Fernet加密存储
|
||||
- [x] 凭据API脱敏 (password_set标记)
|
||||
- [x] 数据库自动备份脚本
|
||||
|
||||
### Sync引擎v2 (4种模式)
|
||||
- [x] S1: 文件同步 (rsync)
|
||||
- [x] S2: 命令批量执行 (SSH)
|
||||
- [x] S3: 配置推送 (sysctl) + 去重 + 回滚
|
||||
- [x] S4: SFTP文件传输
|
||||
|
||||
---
|
||||
|
||||
## 关键Bug修复记录
|
||||
|
||||
| 任务ID | 问题 | 修复内容 |
|
||||
|--------|------|----------|
|
||||
| #81 | DateTime默认求值BUG | 添加`_utcnow()`可调用函数,替换所有`default=datetime.now()` |
|
||||
| #82 | session.py logger未定义 | 添加`import logging`和`logger`定义,移除@property死代码 |
|
||||
| #83 | 审计日志缺失 | 验证所有CUD端点都有审计记录 |
|
||||
| #84 | 模型缺失索引 | 添加7个索引(login_attempts, audit_logs等) |
|
||||
|
||||
---
|
||||
|
||||
## 环境配置
|
||||
|
||||
### 必需环境变量 (.env文件)
|
||||
```bash
|
||||
# 数据库
|
||||
DATABASE_URL=mysql+aiomysql://user:pass@localhost/nexus
|
||||
|
||||
# 安全密钥 (安装时生成)
|
||||
SECRET_KEY=xxx
|
||||
API_KEY=xxx
|
||||
ENCRYPTION_KEY=xxx
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# Telegram告警 (可选)
|
||||
TELEGRAM_BOT_TOKEN=xxx
|
||||
TELEGRAM_CHAT_ID=xxx
|
||||
|
||||
# 告警阈值
|
||||
CPU_ALERT_THRESHOLD=80
|
||||
MEM_ALERT_THRESHOLD=80
|
||||
DISK_ALERT_THRESHOLD=80
|
||||
```
|
||||
|
||||
### 启动模式
|
||||
- **安装模式** (无.env): 仅 `/api/install/` + 静态文件可用
|
||||
- **正常模式** (有.env): 完整初始化 DB/Redis/后台任务
|
||||
|
||||
---
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 安装依赖
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 运行安装向导
|
||||
访问 `http://localhost:8000/app/install.html`
|
||||
- Step 1: 数据库配置
|
||||
- Step 2: Redis配置
|
||||
- Step 3: 安全密钥生成
|
||||
- Step 4: Telegram配置 (可选)
|
||||
- Step 5: 创建管理员账号
|
||||
|
||||
### 3. 启动服务 (Supervisor)
|
||||
```bash
|
||||
# 复制配置
|
||||
cp deploy/supervisor/nexus.conf /etc/supervisor/conf.d/
|
||||
supervisorctl reread
|
||||
supervisorctl update
|
||||
supervisorctl start nexus
|
||||
```
|
||||
|
||||
### 4. 验证运行
|
||||
```bash
|
||||
# 健康检查
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# 运行测试
|
||||
python test_p1_p2.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 团队分工建议
|
||||
|
||||
| 角色 | 职责 | 相关文件 |
|
||||
|------|------|----------|
|
||||
| **后端开发** | API开发、业务逻辑 | `server/api/*.py`, `server/application/services/*.py` |
|
||||
| **前端开发** | UI页面、交互逻辑 | `web/app/*.html`, `web/app/api.js`, `web/app/layout.js` |
|
||||
| **DevOps** | 部署、监控、备份 | `deploy/`, `supervisor/`, `nginx/` |
|
||||
| **DBA** | 数据库优化、迁移 | `server/domain/models/`, `server/infrastructure/database/` |
|
||||
| **安全** | 审计、加密、认证 | `server/api/auth*.py`, 凭据加密逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 监控与告警
|
||||
|
||||
### 3层守护机制
|
||||
1. **Supervisor**: Python崩溃自动重启
|
||||
2. **Python自检** (`self_monitor.py`): 30秒检查Redis/MySQL/WebSocket
|
||||
3. **Shell健康检查** (`health_monitor.sh`): 1分钟HTTP检查,3次失败重启
|
||||
|
||||
### 告警通道
|
||||
- WebSocket → 前端实时通知
|
||||
- Telegram → 手机推送
|
||||
|
||||
### 关键指标
|
||||
- Agent心跳间隔: 60秒
|
||||
- Redis→MySQL刷新: 10分钟
|
||||
- 会话超时: 8小时
|
||||
- 告警冷却: 5分钟
|
||||
|
||||
---
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 已完成的测试
|
||||
```bash
|
||||
# 模块导入测试 (19/19通过)
|
||||
python -c "import server.domain.models; ..."
|
||||
|
||||
# 功能测试 (7/7通过)
|
||||
python test_p1_p2.py
|
||||
# - JWT updated_at claim
|
||||
# - JWT password invalidation
|
||||
# - Alert dedup cooldown
|
||||
# - Server repo pagination
|
||||
# - Config dedup command
|
||||
# - Server dict no password
|
||||
# - Admin.updated_at field
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### ⚠️ 安全相关
|
||||
1. **API_KEY**: 禁止修改,用于Agent认证
|
||||
2. **SECRET_KEY**: 禁止修改,用于JWT签名
|
||||
3. **ENCRYPTION_KEY**: 禁止修改,用于凭据加密
|
||||
4. 生产环境必须修改默认密码
|
||||
|
||||
### ⚠️ 数据库迁移
|
||||
首次启动会自动执行 `_apply_migrations()`:
|
||||
- 添加 `admins.updated_at` 列
|
||||
- 创建7个索引 (如果不存在)
|
||||
|
||||
### ⚠️ 文件权限
|
||||
- SSH私钥文件需要正确权限 (600)
|
||||
- 日志目录需要写权限
|
||||
|
||||
---
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 项目文档: `docs/`
|
||||
- 配置文件: `.env` (不提交到Git)
|
||||
- 测试脚本: `test_p1_p2.py`
|
||||
|
||||
---
|
||||
|
||||
**交接日期**: 2026/05/22
|
||||
**代码状态**: ✅ 开发完成,待部署测试
|
||||
**Git提交**: 建议提交当前worktree更改到主分支
|
||||
历史全文:[docs/archive/project/PROJECT_HANDOVER.md](docs/archive/project/PROJECT_HANDOVER.md)
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# Nexus 6.0
|
||||
|
||||
面向 **2000+ 服务器** 的运维管理平台:FastAPI + Async SQLAlchemy + Redis + WebSocket + Telegram(后端),Vue 3 + Vuetify 4 + TypeScript SPA(前端 14 页)。
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 生产域名 | https://api.synaglobal.vip |
|
||||
| 默认安装目录 | `/opt/nexus` |
|
||||
| 后端端口 | `8600`(1Panel 反代到 `http://127.0.0.1:8600`) |
|
||||
| 安装向导 | `https://你的域名/app/install.html` |
|
||||
| 文档索引 | [docs/README.md](docs/README.md) |
|
||||
|
||||
生产 Docker 栈 **仅含 Nexus 容器**;MySQL / Redis 在宿主机或 1Panel 自行安装。
|
||||
|
||||
---
|
||||
|
||||
## 一键命令速查(root · 公共仓库 · 无需令牌)
|
||||
|
||||
| 场景 | 一条命令 |
|
||||
|------|----------|
|
||||
| **仅装 Nexus**(1Panel 已就绪) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" \| bash` |
|
||||
| **重装服务器**(1Panel + Docker + Nexus) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" \| bash` |
|
||||
| **一键更新**(pull + 重建镜像) | `curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/update.sh" \| bash` |
|
||||
| **强制无缓存重建**(entrypoint 变更后) | `curl -fsSL ".../deploy/update.sh" \| bash -s -- --no-cache` |
|
||||
| **已 clone** | `cd /opt/nexus && bash deploy/update.sh` |
|
||||
|
||||
指定资源档位(`1c4g` / `2c8g` 默认 / `4c16g`):
|
||||
|
||||
```bash
|
||||
curl -fsSL "http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash -s -- --profile 4c16g
|
||||
```
|
||||
|
||||
**raw 地址**:`http://66.154.115.8:3000/admin/Nexus/raw/branch/main/deploy/<脚本名>`
|
||||
|
||||
安装完成后浏览器打开安装向导,完成 MySQL(自建)/ Redis(自建)/ 管理员初始化。
|
||||
|
||||
---
|
||||
|
||||
## NX 统一运维菜单
|
||||
|
||||
装好后在服务器任意目录输入 **`nx`**(`install` / `update` / 首次运行 `nx` 会自动注册 `/usr/local/bin/nx`):
|
||||
|
||||
```bash
|
||||
nx # 统一菜单:安装 + 一键更新 + 重启 + 日志 + 备份
|
||||
nx update # 非交互一键更新
|
||||
nx update --no-cache
|
||||
nx install-fresh # 全新安装
|
||||
nx health # 健康检查
|
||||
```
|
||||
|
||||
| 菜单项 | 作用 |
|
||||
|--------|------|
|
||||
| `[4] 一键更新` | pull + 重建 Docker 镜像(等同 `deploy/update.sh`) |
|
||||
| `[5] 重启 Nexus` | 重启 Python 应用 |
|
||||
| `[6] 日志` | 跟踪容器日志 |
|
||||
| `[1][2]` | 安装 / 全新安装向导 |
|
||||
|
||||
全局命令(符号链接):
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `nx` | 统一运维菜单 |
|
||||
| `nexus-update` | 等同 `deploy/update.sh` |
|
||||
| `nexus-fresh` | 等同 `install-nexus-fresh.sh` |
|
||||
| `nexus-install` | 等同 `quick-install.sh` |
|
||||
|
||||
> `nx god` 已合并进 `nx` 主菜单,仍可作为兼容别名使用。
|
||||
|
||||
---
|
||||
|
||||
## 部署脚本一览(`deploy/`)
|
||||
|
||||
### 1Panel + Docker 生产(主路径)
|
||||
|
||||
| 脚本 | 用途 | curl 入口 |
|
||||
|------|------|-----------|
|
||||
| [quick-install.sh](deploy/quick-install.sh) | **公共仓库安装入口** | ✅ 上表 |
|
||||
| [install-1panel-docker.sh](deploy/install-1panel-docker.sh) | 1Panel + Docker + Nexus 全链 | ✅ 上表 |
|
||||
| [install-nexus-fresh.sh](deploy/install-nexus-fresh.sh) | 全新空库 → 安装向导 | `bash deploy/install-nexus-fresh.sh --skip-clone` |
|
||||
| [update.sh](deploy/update.sh) | **一键更新**(pull + 重建) | ✅ 上表 |
|
||||
| [upgrade-1panel-docker.sh](deploy/upgrade-1panel-docker.sh) | 转调 `nexus-1panel.sh upgrade` | — |
|
||||
| [nexus-1panel.sh](deploy/nexus-1panel.sh) | 安装 / 升级 / check / ports 底层 | — |
|
||||
| [nx](deploy/nx) | **统一交互菜单** | `nx` |
|
||||
| [uninstall-mysql-compose.sh](deploy/uninstall-mysql-compose.sh) | 卸载遗留 Compose MySQL 容器 | — |
|
||||
| [uninstall-redis-compose.sh](deploy/uninstall-redis-compose.sh) | 卸载遗留 Compose Redis 容器 | — |
|
||||
| [sync-install-wizard-to-container.sh](deploy/sync-install-wizard-to-container.sh) | 热同步 install.html 到容器 | — |
|
||||
| [download-install-fresh.sh](deploy/download-install-fresh.sh) | 下载安装脚本(私有库可选 Token) | — |
|
||||
|
||||
详细说明:[deploy/README-1panel.md](deploy/README-1panel.md)
|
||||
|
||||
### 更新选项
|
||||
|
||||
```bash
|
||||
bash deploy/update.sh # 默认:Git 拉取(若有)+ 重建容器
|
||||
bash deploy/update.sh --check # 仅查 Git
|
||||
bash deploy/update.sh --no-cache # 无缓存重建镜像
|
||||
bash deploy/update.sh --no-backup # 跳过 MySQL 备份
|
||||
```
|
||||
|
||||
### Docker Compose 与档位
|
||||
|
||||
| 路径 | 用途 |
|
||||
|------|------|
|
||||
| [docker/docker-compose.prod.yml](docker/docker-compose.prod.yml) | 生产 Compose(**仅 nexus 服务**) |
|
||||
| [docker/profiles/1c4g.env](docker/profiles/1c4g.env) | 1 核 4G |
|
||||
| [docker/profiles/2c8g.env](docker/profiles/2c8g.env) | 2 核 8G(默认) |
|
||||
| [docker/profiles/4c16g.env](docker/profiles/4c16g.env) | 4 核 16G |
|
||||
| [docker/README.md](docker/README.md) | 镜像与本地开发说明 |
|
||||
|
||||
### 经典 SSH / Supervisor 部署(宝塔 / 裸机)
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| [install.sh](deploy/install.sh) | 传统安装 |
|
||||
| [upgrade.sh](deploy/upgrade.sh) | 拉代码 + 重启 |
|
||||
| [deploy-on-server.sh](deploy/deploy-on-server.sh) | 服务器侧部署 |
|
||||
| [deploy-production.sh](deploy/deploy-production.sh) | 生产发布流程 |
|
||||
| [deploy-frontend.sh](deploy/deploy-frontend.sh) | 构建 Vue 前端并打包到 `web/app/` |
|
||||
| [pre_deploy_check.sh](deploy/pre_deploy_check.sh) | **7 道门控** |
|
||||
| [health_monitor.sh](deploy/health_monitor.sh) | cron 健康检查 |
|
||||
| [db_backup.sh](deploy/db_backup.sh) | MySQL 备份 |
|
||||
| [uninstall.sh](deploy/uninstall.sh) | 卸载 |
|
||||
|
||||
---
|
||||
|
||||
## 迁机 vs 全新安装
|
||||
|
||||
| 场景 | 做法 |
|
||||
|------|------|
|
||||
| **新 VPS、空库** | `curl \| quick-install.sh` → 1Panel 自建 MySQL/Redis → `/app/install.html` |
|
||||
| **已有数据、换机** | `nexus-1panel.sh install --from-env` 或 `nx` → 安装向导 |
|
||||
| **仅升级代码/镜像** | `nx update` 或 `bash deploy/update.sh` |
|
||||
| **不必重装 VPS** | `cd /opt/nexus && nx update` |
|
||||
|
||||
每台新服务器自动生成唯一 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`(写入 `docker/.env.prod`,备份 `/root/.nexus-install-secrets-*.env`)。MySQL 密码由自建库时自行设定。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈与目录
|
||||
|
||||
```
|
||||
Nexus/
|
||||
├── server/ # FastAPI 后端(Clean Architecture)
|
||||
├── frontend/ # Vue 3 + Vuetify 4 SPA → 构建到 web/app/
|
||||
├── web/app/ # 静态资源挂载点(构建产物 gitignore)
|
||||
├── deploy/ # 安装、更新、门控、nx 菜单
|
||||
├── docker/ # Compose、镜像、profiles
|
||||
├── docs/ # 设计 / changelog / 审计 / 报告
|
||||
├── tests/ # API 与集成测试
|
||||
└── standards/ # 开发与逐行审计标准
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
uvicorn server.main:app --reload --port 8600
|
||||
|
||||
cd frontend && npm run dev # http://localhost:3000/app/
|
||||
```
|
||||
|
||||
Linux 路径:[docs/project/linux-dev-paths.md](docs/project/linux-dev-paths.md) · [UBUNTU-DEV-README.txt](UBUNTU-DEV-README.txt)
|
||||
|
||||
---
|
||||
|
||||
## 文档与规范
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [docs/README.md](docs/README.md) | 文档总索引 |
|
||||
| [docs/project/nexus-functional-development-guide.md](docs/project/nexus-functional-development-guide.md) | 功能 / API / 14 页 |
|
||||
| [AGENTS.md](AGENTS.md) · [CLAUDE.md](CLAUDE.md) | Agent 薄入口 |
|
||||
|
||||
变更:`docs/changelog/` · 门控:`docs/audit/`
|
||||
|
||||
---
|
||||
|
||||
## 许可与仓库
|
||||
|
||||
| 用途 | 地址 |
|
||||
|------|------|
|
||||
| 仓库首页 | http://66.154.115.8:3000/admin/Nexus |
|
||||
| Git | http://66.154.115.8:3000/admin/Nexus.git |
|
||||
|
||||
分支 `main` · 提交前跑 `deploy/pre_deploy_check.sh`(7 道门控)
|
||||
|
||||
**勿提交**:`deploy/nexus-1panel.secrets.sh`、根目录 `.env`、`SECRETS.md`
|
||||
-425
@@ -1,425 +0,0 @@
|
||||
# Nexus 6.0 团队分工与职责文档
|
||||
|
||||
## 项目团队结构
|
||||
|
||||
```
|
||||
项目经理 (PM)
|
||||
├── 技术负责人 (Tech Lead)
|
||||
│ ├── 后端开发组
|
||||
│ ├── 前端开发组
|
||||
│ └── DevOps组
|
||||
├── 安全负责人 (Security Lead)
|
||||
└── 测试负责人 (QA Lead)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 角色与职责
|
||||
|
||||
### 1. 项目经理 (PM)
|
||||
|
||||
**职责范围**:
|
||||
- 项目进度管理
|
||||
- 需求优先级排序
|
||||
- 跨团队协调
|
||||
- 客户沟通
|
||||
|
||||
**关注文件**:
|
||||
- `CLAUDE.md` - 项目总体进度
|
||||
- `docs/` - 需求文档
|
||||
- `PROJECT_HANDOVER.md` - 项目状态
|
||||
|
||||
---
|
||||
|
||||
### 2. 技术负责人 (Tech Lead)
|
||||
|
||||
**职责范围**:
|
||||
- 技术架构决策
|
||||
- 代码审查
|
||||
- 技术债务管理
|
||||
- 团队技术指导
|
||||
|
||||
**关键文件**:
|
||||
- `server/` - 后端架构
|
||||
- `web/app/` - 前端架构
|
||||
- `deploy/` - 部署架构
|
||||
|
||||
---
|
||||
|
||||
### 3. 后端开发组
|
||||
|
||||
#### 3.1 API开发工程师
|
||||
|
||||
**负责模块**:
|
||||
```
|
||||
server/api/
|
||||
├── agent.py # Agent心跳接收 (优先级: 高)
|
||||
├── auth.py # 认证授权 (优先级: 高)
|
||||
├── auth_jwt.py # JWT中间件 (优先级: 高)
|
||||
├── servers.py # 服务器CRUD (优先级: 高)
|
||||
├── sync_v2.py # 同步引擎API (优先级: 高)
|
||||
├── scripts.py # 脚本管理 (优先级: 中)
|
||||
├── assets.py # 资产管理 (优先级: 中)
|
||||
├── settings.py # 系统设置 (优先级: 中)
|
||||
├── push.py # 推送调度 (优先级: 中)
|
||||
├── files.py # 文件浏览 (优先级: 低)
|
||||
├── webssh.py # WebSSH (优先级: 低)
|
||||
└── websocket.py # WebSocket (优先级: 低)
|
||||
```
|
||||
|
||||
**技能要求**:
|
||||
- FastAPI / Python 3.9+
|
||||
- Async SQLAlchemy 2.0
|
||||
- Pydantic 数据验证
|
||||
- JWT认证流程
|
||||
|
||||
#### 3.2 服务层工程师
|
||||
|
||||
**负责模块**:
|
||||
```
|
||||
server/application/services/
|
||||
├── sync_engine_v2.py # 同步引擎v2 (核心)
|
||||
├── auth_service.py # 认证服务
|
||||
├── server_service.py # 服务器服务
|
||||
└── ...
|
||||
```
|
||||
|
||||
**技能要求**:
|
||||
- 业务逻辑设计
|
||||
- 异步编程 (asyncio)
|
||||
- SSH连接池管理
|
||||
- 错误处理策略
|
||||
|
||||
#### 3.3 数据层工程师
|
||||
|
||||
**负责模块**:
|
||||
```
|
||||
server/infrastructure/database/
|
||||
├── server_repo.py # 服务器Repository
|
||||
├── audit_log_repo.py # 审计日志Repository
|
||||
├── admin_repo.py # 管理员Repository
|
||||
├── sync_log_repo.py # 同步日志Repository
|
||||
└── ...
|
||||
|
||||
server/domain/models/
|
||||
└── __init__.py # SQLAlchemy模型 (14张表)
|
||||
```
|
||||
|
||||
**技能要求**:
|
||||
- SQLAlchemy ORM
|
||||
- MySQL优化
|
||||
- 索引设计
|
||||
- 数据库迁移
|
||||
|
||||
#### 3.4 基础设施工程师
|
||||
|
||||
**负责模块**:
|
||||
```
|
||||
server/infrastructure/
|
||||
├── ssh/
|
||||
│ └── asyncssh_pool.py # SSH连接池 (关键)
|
||||
├── redis/
|
||||
│ └── client.py # Redis客户端
|
||||
├── telegram/
|
||||
│ └── __init__.py # Telegram推送
|
||||
└── database/
|
||||
└── session.py # 数据库会话管理
|
||||
```
|
||||
|
||||
**技能要求**:
|
||||
- asyncssh 库
|
||||
- Redis操作
|
||||
- 网络编程
|
||||
- 异步并发
|
||||
|
||||
---
|
||||
|
||||
### 4. 前端开发组
|
||||
|
||||
#### 4.1 页面开发工程师
|
||||
|
||||
**负责页面**:
|
||||
```
|
||||
web/app/
|
||||
├── login.html # 登录页 (优先级: 高)
|
||||
├── index.html # 仪表盘 (优先级: 高)
|
||||
├── servers.html # 服务器管理 (优先级: 高)
|
||||
├── assets.html # 资产管理 (优先级: 中)
|
||||
├── push.html # 推送页面 (优先级: 中)
|
||||
├── scripts.html # 脚本管理 (优先级: 中)
|
||||
├── commands.html # 命令日志 (优先级: 中)
|
||||
├── files.html # 文件浏览 (优先级: 低)
|
||||
├── settings.html # 系统设置 (优先级: 低)
|
||||
├── audit.html # 审计日志 (优先级: 低)
|
||||
├── retries.html # 重试队列 (优先级: 低)
|
||||
├── terminal.html # WebSSH终端 (优先级: 低)
|
||||
└── install.html # 安装向导 (一次性)
|
||||
```
|
||||
|
||||
**技能要求**:
|
||||
- HTML5 / CSS3
|
||||
- Tailwind CSS v4
|
||||
- Alpine.js 框架
|
||||
- Fetch API
|
||||
|
||||
#### 4.2 共享组件工程师
|
||||
|
||||
**负责文件**:
|
||||
```
|
||||
web/app/
|
||||
├── api.js # API模块 (JWT+自动刷新+Toast)
|
||||
├── layout.js # 共享布局 (侧边栏+导航)
|
||||
└── components/ # 可复用组件
|
||||
```
|
||||
|
||||
**技能要求**:
|
||||
- JavaScript模块化
|
||||
- LocalStorage操作
|
||||
- 事件驱动编程
|
||||
- 响应式设计
|
||||
|
||||
---
|
||||
|
||||
### 5. DevOps组
|
||||
|
||||
#### 5.1 部署工程师
|
||||
|
||||
**负责文件**:
|
||||
```
|
||||
deploy/
|
||||
├── supervisor/
|
||||
│ └── nexus.conf # Supervisor配置
|
||||
├── nginx/
|
||||
│ └── nexus.conf # Nginx配置
|
||||
├── db_backup.sh # 数据库备份脚本
|
||||
└── health_monitor.sh # 健康检查脚本
|
||||
```
|
||||
|
||||
**职责**:
|
||||
- 服务器环境搭建
|
||||
- 自动化部署脚本
|
||||
- 配置管理
|
||||
- 日志收集
|
||||
|
||||
**技能要求**:
|
||||
- Linux系统管理
|
||||
- Supervisor进程管理
|
||||
- Nginx反向代理
|
||||
- Shell脚本
|
||||
|
||||
#### 5.2 监控工程师
|
||||
|
||||
**负责模块**:
|
||||
```
|
||||
server/background/
|
||||
├── self_monitor.py # Python自检
|
||||
├── heartbeat_flush.py # 心跳数据刷新
|
||||
├── scheduler.py # 定时任务调度
|
||||
└── retry_worker.py # 重试队列处理
|
||||
```
|
||||
|
||||
**职责**:
|
||||
- 系统监控告警
|
||||
- 性能指标收集
|
||||
- 故障自动恢复
|
||||
- 容量规划
|
||||
|
||||
**技能要求**:
|
||||
- Python后台任务
|
||||
- Redis数据流
|
||||
- 监控工具 (Prometheus/Grafana)
|
||||
- 告警策略
|
||||
|
||||
---
|
||||
|
||||
### 6. 安全负责人 (Security Lead)
|
||||
|
||||
**负责领域**:
|
||||
|
||||
#### 6.1 认证授权
|
||||
```
|
||||
server/api/auth.py
|
||||
server/api/auth_jwt.py
|
||||
server/application/services/auth_service.py
|
||||
```
|
||||
|
||||
**关注点**:
|
||||
- JWT安全 (密钥管理、过期策略)
|
||||
- TOTP双因素认证
|
||||
- 登录防暴破机制
|
||||
- 会话安全 (8小时超时)
|
||||
|
||||
#### 6.2 数据加密
|
||||
```
|
||||
server/api/servers.py # 凭据加密
|
||||
server/infrastructure/ # 加密实现
|
||||
```
|
||||
|
||||
**关注点**:
|
||||
- Fernet对称加密
|
||||
- 密钥安全存储
|
||||
- API脱敏处理
|
||||
- 传输层安全 (HTTPS)
|
||||
|
||||
#### 6.3 审计合规
|
||||
```
|
||||
server/infrastructure/database/audit_log_repo.py
|
||||
server/api/ # 审计日志调用
|
||||
```
|
||||
|
||||
**关注点**:
|
||||
- 操作审计全覆盖
|
||||
- 敏感操作记录
|
||||
- 审计日志查询
|
||||
- 合规性检查
|
||||
|
||||
**技能要求**:
|
||||
- 安全架构设计
|
||||
- 加密算法
|
||||
- 安全审计
|
||||
- 漏洞评估
|
||||
|
||||
---
|
||||
|
||||
### 7. 测试负责人 (QA Lead)
|
||||
|
||||
**负责领域**:
|
||||
|
||||
#### 7.1 自动化测试
|
||||
```
|
||||
tests/
|
||||
├── unit/ # 单元测试
|
||||
├── integration/ # 集成测试
|
||||
└── e2e/ # 端到端测试
|
||||
|
||||
test_p1_p2.py # 功能验证脚本
|
||||
```
|
||||
|
||||
#### 7.2 测试策略
|
||||
|
||||
| 测试类型 | 覆盖范围 | 工具 |
|
||||
|----------|----------|------|
|
||||
| 单元测试 | Repository, Service | pytest |
|
||||
| API测试 | 所有API端点 | pytest + httpx |
|
||||
| 集成测试 | 数据库、Redis、SSH | pytest |
|
||||
| E2E测试 | 关键用户流程 | Playwright |
|
||||
|
||||
**技能要求**:
|
||||
- Python测试框架
|
||||
- API测试
|
||||
- 性能测试
|
||||
- 测试覆盖率
|
||||
|
||||
---
|
||||
|
||||
## 代码审查分工
|
||||
|
||||
### 审查矩阵
|
||||
|
||||
| 模块 | 主审查人 | 次审查人 |
|
||||
|------|----------|----------|
|
||||
| API层 | Tech Lead | 后端组长 |
|
||||
| Service层 | 后端组长 | Tech Lead |
|
||||
| Repository层 | 数据层工程师 | Tech Lead |
|
||||
| 前端页面 | 前端组长 | Tech Lead |
|
||||
| 共享组件 | Tech Lead | 前端组长 |
|
||||
| 部署脚本 | DevOps组长 | Tech Lead |
|
||||
| 安全配置 | Security Lead | Tech Lead |
|
||||
|
||||
### 审查清单
|
||||
|
||||
**后端代码审查**:
|
||||
- [ ] 异步函数正确使用 async/await
|
||||
- [ ] 数据库会话正确关闭
|
||||
- [ ] 异常处理完善
|
||||
- [ ] 审计日志记录
|
||||
- [ ] JWT权限检查
|
||||
- [ ] 输入参数验证
|
||||
|
||||
**前端代码审查**:
|
||||
- [ ] API错误处理
|
||||
- [ ] 加载状态显示
|
||||
- [ ] 空状态处理
|
||||
- [ ] 响应式布局
|
||||
- [ ] XSS防护
|
||||
|
||||
---
|
||||
|
||||
## 沟通机制
|
||||
|
||||
### 日常沟通
|
||||
- **站会**: 每日10:00,15分钟
|
||||
- **频道**: 飞书/钉钉项目群
|
||||
- **文档**: 飞书文档/Confluence
|
||||
|
||||
### 技术决策
|
||||
- **架构评审**: Tech Lead + 相关开发
|
||||
- **安全评审**: Security Lead + Tech Lead
|
||||
- **代码审查**: 至少1人批准
|
||||
|
||||
### 问题升级
|
||||
```
|
||||
Level 1: 开发自行解决 (2小时内)
|
||||
Level 2: 小组内讨论 (4小时内)
|
||||
Level 3: Tech Lead介入 (1天内)
|
||||
Level 4: 项目会议决策 (2天内)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 知识传递
|
||||
|
||||
### 关键知识领域
|
||||
|
||||
| 领域 | 负责人 | 备份人 | 文档位置 |
|
||||
|------|--------|--------|----------|
|
||||
| 同步引擎 | 后端组长 | Tech Lead | `server/application/services/sync_engine_v2.py` |
|
||||
| SSH连接池 | 基础设施工程师 | Tech Lead | `server/infrastructure/ssh/asyncssh_pool.py` |
|
||||
| JWT认证 | Security Lead | 后端组长 | `server/api/auth_jwt.py` |
|
||||
| 前端架构 | 前端组长 | Tech Lead | `web/app/api.js`, `layout.js` |
|
||||
| 部署流程 | DevOps组长 | Tech Lead | `deploy/` |
|
||||
|
||||
### 培训计划
|
||||
|
||||
**新员工入职**:
|
||||
1. 项目架构介绍 (Tech Lead, 2小时)
|
||||
2. 代码规范培训 (各组长, 2小时)
|
||||
3. 开发环境搭建 (DevOps, 4小时)
|
||||
4. 第一个任务指导 (导师制, 1周)
|
||||
|
||||
---
|
||||
|
||||
## 交付物清单
|
||||
|
||||
### 已完成 ✅
|
||||
- [x] 后端API (15个模块)
|
||||
- [x] 前端页面 (15个HTML)
|
||||
- [x] 数据库模型 (14张表)
|
||||
- [x] 同步引擎v2 (4种模式)
|
||||
- [x] 后台任务 (4个模块)
|
||||
- [x] 部署配置 (Supervisor+Nginx)
|
||||
- [x] 验证测试脚本
|
||||
|
||||
### 待开始 ⏳
|
||||
- [ ] 生产环境部署
|
||||
- [ ] 压力测试
|
||||
- [ ] 安全渗透测试
|
||||
- [ ] 用户验收测试
|
||||
- [ ] 运维文档完善
|
||||
|
||||
---
|
||||
|
||||
## 风险与应对
|
||||
|
||||
| 风险 | 概率 | 影响 | 应对措施 |
|
||||
|------|------|------|----------|
|
||||
| SSH连接池性能瓶颈 | 中 | 高 | 监控连接数,调优池大小 |
|
||||
| Redis单点故障 | 低 | 高 | 配置Redis Sentinel集群 |
|
||||
| MySQL性能瓶颈 | 中 | 中 | 读写分离,索引优化 |
|
||||
| 前端兼容性问题 | 中 | 低 | 多浏览器测试 |
|
||||
| 安全漏洞 | 低 | 高 | 定期安全扫描 |
|
||||
|
||||
---
|
||||
|
||||
**文档维护**: Tech Lead
|
||||
**最后更新**: 2026/05/22
|
||||
@@ -0,0 +1,49 @@
|
||||
Nexus — Ubuntu 离线开发包
|
||||
========================
|
||||
|
||||
解压:
|
||||
mkdir -p ~/Nexus && tar xzf nexus-ubuntu-dev-2026-06-03.tar.gz -C ~/
|
||||
cd ~/Nexus
|
||||
|
||||
Python 后端:
|
||||
sudo apt install -y python3 python3-venv python3-pip git curl build-essential
|
||||
python3 -m venv .venv && source .venv/bin/activate
|
||||
pip install -r requirements.txt -r requirements-dev.txt
|
||||
# 复制已有 .env,或执行下方 generate + sync_root_env
|
||||
|
||||
MySQL + Redis(Docker):
|
||||
sudo apt install -y docker.io docker-compose-v2
|
||||
sudo usermod -aG docker "$USER" # 重新登录后免 sudo docker
|
||||
python3 docker/generate_env.py
|
||||
python3 docker/sync_root_env.py # 生成根目录 .env(127.0.0.1 + 较小连接池)
|
||||
docker compose up -d mysql redis # Redis 已映射 6379 供宿主机 uvicorn
|
||||
# 首次空库:先不要 sync_root_env,无 .env 启动 uvicorn 走 /app/install.html 向导
|
||||
|
||||
启动 API:
|
||||
bash scripts/start-dev.sh
|
||||
# 或一键验证:bash scripts/local_verify.sh
|
||||
|
||||
前端(Node 20+):
|
||||
cd frontend && npm ci && npx vite build
|
||||
|
||||
编码 / 换行(Windows tar 解压后必做):
|
||||
git config core.autocrlf false
|
||||
bash scripts/normalize_worktree_lf.sh
|
||||
python3 scripts/check_worktree_eol.py
|
||||
python3 scripts/check_text_eol.py
|
||||
详见 docs/audit/2026-06-04-encoding-review-ubuntu-phase1.md
|
||||
|
||||
SOCKS5 代理(拉 Docker / npm 镜像):
|
||||
默认 192.168.124.93:10808 — 改 scripts/proxychains-docker.conf 或环境变量
|
||||
scripts/with-proxy.sh docker compose pull
|
||||
cd frontend && ../scripts/with-proxy.sh npm ci
|
||||
source scripts/proxy-env.sh # pip/curl 等
|
||||
|
||||
本地管理员(空库一次):
|
||||
# .env 增加 NEXUS_TEST_ADMIN_PASSWORD=你的密码
|
||||
python3 scripts/seed_local_admin.py
|
||||
cd frontend && NEXUS_E2E_BASE=http://127.0.0.1:8600 NEXUS_E2E_PASSWORD=... npm run test:e2e
|
||||
|
||||
工作区路径:~/Nexus
|
||||
路径 SSOT:docs/project/linux-dev-paths.md · docs/project/local-integration-test.md
|
||||
Git 远程:http://66.154.115.8:3000/admin/Nexus.git(网络恢复后可 git pull)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,71 @@
|
||||
# 1Panel OpenResty — Nexus reverse proxy (paste into site custom config)
|
||||
# Domain: api.synaglobal.vip
|
||||
# Upstream: Nexus Docker on 127.0.0.1:8600 (docker-compose.prod.yml)
|
||||
#
|
||||
# SSL: configure in 1Panel UI (Let's Encrypt). This snippet assumes HTTPS server block.
|
||||
|
||||
upstream nexus_api {
|
||||
server 127.0.0.1:8600;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# WebSocket log without query string (JWT must not hit access_log)
|
||||
log_format nexus_ws '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent';
|
||||
|
||||
client_max_body_size 500m;
|
||||
client_body_timeout 600s;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_connect_timeout 10s;
|
||||
}
|
||||
|
||||
location /ws/ {
|
||||
access_log /www/sites/api.synaglobal.vip/log/ws.log nexus_ws;
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /app/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /agent/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Fallback: install mode or misc routes served by FastAPI
|
||||
location / {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
# 1Panel + Docker 一键部署
|
||||
|
||||
> 完整知识文档:[docs/project/nexus-1panel-operations-knowledge.md](../docs/project/nexus-1panel-operations-knowledge.md)
|
||||
> 巡检归档(2026-06-06):[docs/project/nexus-god-menu-audit-summary.md](../docs/project/nexus-god-menu-audit-summary.md)
|
||||
|
||||
## 脚本速查(公共仓库 · 无需令牌)
|
||||
|
||||
| 脚本 | 用途 | 一条命令 |
|
||||
|------|------|----------|
|
||||
| `quick-install.sh` | 仅装 Nexus(1Panel 已就绪) | `curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" \| bash` |
|
||||
| `install-1panel-docker.sh` | 重装服务器全链 | `curl -fsSL ".../install-1panel-docker.sh" \| bash` |
|
||||
| `install-nexus-fresh.sh` | 全新空库安装 | `bash deploy/install-nexus-fresh.sh --skip-clone` |
|
||||
| **`update.sh`** | **一键更新** | `curl -fsSL ".../update.sh" \| bash` |
|
||||
| `nexus-1panel.sh` | 底层 install / upgrade | `bash deploy/nexus-1panel.sh upgrade` |
|
||||
| `fix-1panel-mysql-grant.sh` | 修复 1045 nexus@Docker | `bash deploy/fix-1panel-mysql-grant.sh` |
|
||||
| `test-1panel-redis.sh` | Redis 认证诊断 | `REDIS_PASS='...' bash deploy/test-1panel-redis.sh` |
|
||||
| `verify-1panel-install-wizard.sh` | 一键验收(网络/cron/镜像) | `bash deploy/verify-1panel-install-wizard.sh` |
|
||||
| `install_ops_cron.sh` | 宿主机 cron(巡检+备份) | `sudo bash deploy/install_ops_cron.sh` |
|
||||
| `mysql_dump_to_file.sh` | MySQL 备份(Docker exec) | 见 `db_backup.sh` / `nx` 菜单 `[9]` |
|
||||
| `upgrade.sh` | 自动识别 Docker/Supervisor | `bash deploy/upgrade.sh` |
|
||||
| **`nx`** | **统一运维菜单** | `nx` |
|
||||
|
||||
档位:`--profile 1c4g` · `2c8g`(默认)· `4c16g`
|
||||
|
||||
---
|
||||
|
||||
## 重装服务器(1Panel + Docker + Nexus)
|
||||
|
||||
```bash
|
||||
curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
```
|
||||
|
||||
4 核 16G:
|
||||
|
||||
```bash
|
||||
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g
|
||||
```
|
||||
|
||||
已装 1Panel,只装 Nexus:
|
||||
|
||||
```bash
|
||||
curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
```
|
||||
|
||||
## 仅 Nexus Docker
|
||||
|
||||
```bash
|
||||
curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
```
|
||||
|
||||
安装完成后:`https://api.synaglobal.vip/app/install.html`
|
||||
|
||||
---
|
||||
|
||||
## 一键更新(日常)
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/update.sh
|
||||
# 或
|
||||
nx update
|
||||
# 或菜单
|
||||
nx # → [4] 一键更新
|
||||
```
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `--check` | 仅查 Git,不重建 |
|
||||
| `--no-cache` | 无缓存重建镜像(entrypoint 变更后必用) |
|
||||
| `--no-backup` | 跳过 MySQL 备份 |
|
||||
| `--prune` | 清理悬空镜像 |
|
||||
|
||||
远程:
|
||||
|
||||
```bash
|
||||
curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
|
||||
curl -fsSL ".../update.sh" | bash -s -- --no-cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NX 全局命令
|
||||
|
||||
安装 / 升级 / 首次运行 `nx` 会自动注册到 `/usr/local/bin`:
|
||||
|
||||
```bash
|
||||
# 手动注册(任意目录可用 nx)
|
||||
NEXUS_ROOT=/opt/nexus bash /opt/nexus/deploy/install-nx-cli.sh
|
||||
|
||||
nx # 统一运维菜单
|
||||
nexus-update # 等同 update.sh
|
||||
nexus-fresh # 等同 install-nexus-fresh.sh
|
||||
nexus-install # 等同 quick-install.sh
|
||||
```
|
||||
|
||||
| 菜单 / 子命令 | 功能 |
|
||||
|---------------|------|
|
||||
| `[1]` | 安装向导(档位 / 迁机 / skip-clone) |
|
||||
| `[2]` | 全新安装 → `/app/install.html` |
|
||||
| `[3]` | 状态 + `/health` + install.html 归档检查 |
|
||||
| `[4]` / `nx update` | **一键更新**(备份 + 重建镜像 + 升级后验收) |
|
||||
| `[5]` | 重启 Nexus 容器 |
|
||||
| `[6]` | 日志 |
|
||||
| `[7]` | 启动 / 停止 Compose 栈 |
|
||||
| `[8]` | 无缓存重建镜像 |
|
||||
| `[9]` | 立即备份 MySQL |
|
||||
| `[e]` / `nx cron` | 安装 health_monitor + db_backup crontab |
|
||||
| `[v]` / `nx verify` | 运行 `verify-1panel-install-wizard.sh` |
|
||||
|
||||
---
|
||||
|
||||
## 三层守护(1Panel Docker)
|
||||
|
||||
| 层 | 机制 | 说明 |
|
||||
|----|------|------|
|
||||
| Layer 1 | Compose `restart` + 容器 `healthcheck` | 容器崩溃自动拉起 |
|
||||
| Layer 2 | Python `self_monitor`(30s) | Redis/MySQL/WebSocket 自检 |
|
||||
| Layer 3 | 宿主机 `health_monitor.sh`(cron) | `/health` 连续失败 → `docker restart` + 可选 Telegram |
|
||||
|
||||
Layer 3 需**在宿主机**安装(容器内无法写 crontab):
|
||||
|
||||
```bash
|
||||
sudo nx cron
|
||||
# 或
|
||||
sudo bash /opt/nexus/deploy/install_ops_cron.sh
|
||||
```
|
||||
|
||||
MySQL 定时备份:每日 03:00 → `/var/backups/nexus/`(`docker exec` 进 1Panel MySQL 容器 dump,**无需**宿主机 `mysqldump`)。
|
||||
|
||||
---
|
||||
|
||||
## 安装完成后
|
||||
|
||||
- 向导锁定后 `install.html` → **`install.html.bak`**,外网 `/app/install.html` 返回 **404**(正常)
|
||||
- 登录:`https://你的域名/app/`
|
||||
- 验收:`sudo nx verify`
|
||||
|
||||
---
|
||||
|
||||
## MySQL / Redis(1Panel 应用商店)
|
||||
|
||||
Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安装。
|
||||
|
||||
### 1Panel 官方互联方式(必读)
|
||||
|
||||
[1Panel 维护者说明](https://github.com/1Panel-dev/1Panel/issues/11676):应用商店应用使用 **bridge + `1panel-network`**;**容器之间用容器名通信**(Docker 内置 DNS),不要用 `localhost` / `127.0.0.1` / `host.docker.internal`。
|
||||
|
||||
| 场景 | 正确主机 |
|
||||
|------|----------|
|
||||
| Nexus → 1Panel MySQL | `1Panel-mysql-xxxx`(容器列表中的名称) |
|
||||
| Nexus → 1Panel Redis | `1Panel-redis-xxxx` |
|
||||
| OpenResty → 应用 | `127.0.0.1:端口`(OpenResty 为 host 网络) |
|
||||
|
||||
`nx update` / `nexus-1panel.sh` 会:
|
||||
|
||||
1. 将 Nexus 加入 **`1panel-network`**(`docker/docker-compose.1panel.yml`)
|
||||
2. 自动探测 MySQL/Redis 容器名写入 `docker/.env.prod`(含 `NEXUS_REDIS_URL=redis://1Panel-redis-xxxx:6379/0`)
|
||||
3. 安装向导步骤 3 **预填 MySQL/Redis 容器名**
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **应用商店** → 安装 **MySQL**、**Redis**
|
||||
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
|
||||
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`)
|
||||
4. 填写 MySQL 密码;Redis:**用户名通常留空** + 应用参数密码(自动尝试 `:pass@` / `default`)
|
||||
|
||||
### 常见错误 1045(Access denied for 'nexus'@'172.18.x.x')
|
||||
|
||||
TCP 已通,但 MySQL 用户多为 **`nexus@localhost`**,Docker 内 Nexus 从 **172.x** 连接会被拒绝。
|
||||
|
||||
在服务器执行(按提示输入 root 密码与 nexus 密码):
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/fix-1panel-mysql-grant.sh
|
||||
```
|
||||
|
||||
或一次性:
|
||||
|
||||
```bash
|
||||
MYSQL_ROOT_PASSWORD='1Panel里MySQL的root密码' \
|
||||
NEXUS_DB_PASSWORD='向导里要填的nexus密码' \
|
||||
bash /opt/nexus/deploy/fix-1panel-mysql-grant.sh
|
||||
```
|
||||
|
||||
然后安装向导步骤 3:**库名/用户 `nexus`**,主机为 `1Panel-mysql-xxxx`,密码与上面一致。
|
||||
|
||||
若仍报 `Can't connect to MySQL/Redis on 'host.docker.internal'`:说明 Nexus **未接入 1panel-network** 或未探测到容器名,在服务器执行:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
步骤 2 会 TCP 检测 MySQL/Redis;步骤 4 验证账号密码。
|
||||
|
||||
卸载旧 Compose 容器:
|
||||
|
||||
```bash
|
||||
bash deploy/uninstall-mysql-compose.sh
|
||||
bash deploy/uninstall-redis-compose.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1Panel 反代
|
||||
|
||||
**禁止**脚本改 `/opt/1panel/apps/openresty/`。
|
||||
|
||||
1. 应用商店 → **OpenResty**
|
||||
2. 网站 → 反代 `http://127.0.0.1:8600`
|
||||
3. 参考:`deploy/1panel/openresty-nexus.conf.example`
|
||||
|
||||
---
|
||||
|
||||
## 密钥
|
||||
|
||||
新机自动生成 `NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY` → `docker/.env.prod`,备份 `/root/.nexus-install-secrets-*.env`。
|
||||
|
||||
MySQL 密码在 1Panel 自建库时设定,向导步骤 3 填写。
|
||||
|
||||
| 场景 | 命令 |
|
||||
|------|------|
|
||||
| 新机 | `curl \| quick-install.sh` |
|
||||
| 迁机 | `nexus-1panel.sh install --from-env /path/to/.env` |
|
||||
| 更新 | `update.sh` 或 `nx update` |
|
||||
|
||||
**Git**:公开仓库 `clone` / `pull` / `curl | bash` 无需令牌。
|
||||
|
||||
---
|
||||
|
||||
## 安装向导验收(服务器执行)
|
||||
|
||||
在 **1Panel 终端**(root)一键检查网络、容器名预填、cron、镜像版本、本地 `/health`:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && sudo nx verify
|
||||
```
|
||||
|
||||
**未安装**:通过后再打开 `https://你的域名/app/install.html`。
|
||||
**已安装**:`install.html` 应 HTTP 404;验收检查 crontab、`mysql_dump_to_file.sh`、容器内 round6/7 代码特性。
|
||||
|
||||
常见修复:
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && bash deploy/detect-1panel-services.sh
|
||||
nx update --no-cache
|
||||
```
|
||||
|
||||
外网 HTTPS 仍失败但脚本本地检查全绿 → 在 1Panel **网站**配置反代 `http://127.0.0.1:8600`(见 `deploy/1panel/openresty-nexus.conf.example`)。
|
||||
|
||||
## 安装向导热修复
|
||||
|
||||
```bash
|
||||
cd /opt/nexus && git pull
|
||||
bash deploy/update.sh --no-cache
|
||||
# 或仅同步静态:
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env bash
|
||||
# Apply a Nexus release tarball on the production host.
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash deploy/apply-release-bundle.sh \
|
||||
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
||||
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1 \
|
||||
# --deploy-path /opt/nexus
|
||||
#
|
||||
# Validate only (no backup, no rsync, no rebuild):
|
||||
# bash deploy/apply-release-bundle.sh --validate-only \
|
||||
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
||||
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1
|
||||
#
|
||||
# This script intentionally preserves runtime secrets and data:
|
||||
# - .env
|
||||
# - docker/.env.prod
|
||||
# - web/data
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TARBALL=""
|
||||
EXPECTED_SHA256=""
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
||||
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/opt/nexus-backups}"
|
||||
SKIP_REBUILD=0
|
||||
DRY_RUN=0
|
||||
VALIDATE_ONLY=0
|
||||
APPLY_TMP=""
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
usage() {
|
||||
sed -n '1,26p' "$0"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--tarball)
|
||||
TARBALL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--sha256)
|
||||
EXPECTED_SHA256="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--deploy-path)
|
||||
DEPLOY_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--backup-dir)
|
||||
BACKUP_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--skip-rebuild)
|
||||
SKIP_REBUILD=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--validate-only)
|
||||
VALIDATE_ONLY=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error "Unknown argument: $1"
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
error "Missing required command: $1"
|
||||
exit 127
|
||||
}
|
||||
}
|
||||
|
||||
require_runtime_cmds() {
|
||||
require_cmd tar
|
||||
require_cmd sha256sum
|
||||
if [[ "$VALIDATE_ONLY" == 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
require_cmd rsync
|
||||
require_cmd curl
|
||||
}
|
||||
|
||||
run() {
|
||||
echo "+ $*"
|
||||
if [[ "$DRY_RUN" == 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${APPLY_TMP:-}" && -d "$APPLY_TMP" ]]; then
|
||||
rm -rf "$APPLY_TMP"
|
||||
fi
|
||||
}
|
||||
|
||||
detect_runtime() {
|
||||
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
|
||||
echo docker
|
||||
return
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo supervisor
|
||||
return
|
||||
fi
|
||||
echo unknown
|
||||
}
|
||||
|
||||
verify_sha256() {
|
||||
if [[ -z "$EXPECTED_SHA256" ]]; then
|
||||
warn "No --sha256 provided; skipping checksum verification"
|
||||
return 0
|
||||
fi
|
||||
local actual
|
||||
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
|
||||
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
|
||||
error "SHA256 mismatch"
|
||||
error "expected: $EXPECTED_SHA256"
|
||||
error "actual: $actual"
|
||||
exit 1
|
||||
fi
|
||||
info "SHA256 OK: $actual"
|
||||
}
|
||||
|
||||
validate_extracted_tree() {
|
||||
local src="$1"
|
||||
for required in server deploy Dockerfile.prod frontend-v2 web/app-v2; do
|
||||
if [[ ! -e "${src}/${required}" ]]; then
|
||||
error "Release bundle missing required path: ${required}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
for forbidden in .env docker/.env.prod web/data .venv-py312-codex frontend-v2/node_modules 2025.2 =2025.2; do
|
||||
if [[ -e "${src}/${forbidden}" ]]; then
|
||||
error "Release bundle contains forbidden path: ${forbidden}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
info "Extracted release tree validation OK"
|
||||
}
|
||||
|
||||
backup_current() {
|
||||
if [[ ! -d "$DEPLOY_PATH" ]]; then
|
||||
warn "Deploy path does not exist yet: $DEPLOY_PATH"
|
||||
return 0
|
||||
fi
|
||||
local stamp backup
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
backup="${BACKUP_DIR}/nexus-before-${stamp}.tar.gz"
|
||||
run mkdir -p "$BACKUP_DIR"
|
||||
info "Backup current deploy path -> $backup"
|
||||
run tar czf "$backup" \
|
||||
--exclude='./web/data' \
|
||||
--exclude='./.venv*' \
|
||||
--exclude='./venv' \
|
||||
--exclude='./frontend/node_modules' \
|
||||
--exclude='./frontend-v2/node_modules' \
|
||||
-C "$(dirname "$DEPLOY_PATH")" "$(basename "$DEPLOY_PATH")"
|
||||
}
|
||||
|
||||
sync_release() {
|
||||
local src="$1"
|
||||
run mkdir -p "$DEPLOY_PATH"
|
||||
info "Sync release tree -> $DEPLOY_PATH"
|
||||
run rsync -a --delete \
|
||||
--exclude='.env' \
|
||||
--exclude='docker/.env.prod' \
|
||||
--exclude='web/data' \
|
||||
"${src}/" "${DEPLOY_PATH}/"
|
||||
}
|
||||
|
||||
rebuild_or_restart() {
|
||||
if [[ "$SKIP_REBUILD" == 1 ]]; then
|
||||
warn "--skip-rebuild set; not rebuilding/restarting Nexus"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local runtime
|
||||
runtime="$(detect_runtime)"
|
||||
info "Runtime: $runtime"
|
||||
case "$runtime" in
|
||||
docker)
|
||||
if [[ -x "${DEPLOY_PATH}/deploy/nexus-1panel.sh" || -f "${DEPLOY_PATH}/deploy/nexus-1panel.sh" ]]; then
|
||||
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/nexus-1panel.sh" upgrade --skip-git
|
||||
else
|
||||
error "Docker runtime detected but deploy/nexus-1panel.sh missing"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -f "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" ]]; then
|
||||
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh"
|
||||
fi
|
||||
;;
|
||||
supervisor)
|
||||
run supervisorctl restart nexus
|
||||
;;
|
||||
*)
|
||||
error "Unknown runtime. Rebuild/restart manually."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
verify_local_endpoints() {
|
||||
local port health app app_v2
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
port="${port:-8600}"
|
||||
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
||||
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
|
||||
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
|
||||
echo " /health -> ${health}"
|
||||
echo " /app/ -> ${app}"
|
||||
echo " /app-v2/ -> ${app_v2}"
|
||||
if [[ "$health" != "ok" || "$app" != "200" || "$app_v2" != "200" ]]; then
|
||||
error "Local endpoint verification failed"
|
||||
exit 1
|
||||
fi
|
||||
info "Local endpoint verification OK"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ -z "$TARBALL" ]]; then
|
||||
error "--tarball is required"
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
[[ -f "$TARBALL" ]] || {
|
||||
error "Tarball not found: $TARBALL"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_runtime_cmds
|
||||
|
||||
info "Tarball: $TARBALL"
|
||||
info "Deploy path: $DEPLOY_PATH"
|
||||
info "Backup dir: $BACKUP_DIR"
|
||||
verify_sha256
|
||||
|
||||
local extracted
|
||||
APPLY_TMP="$(mktemp -d /tmp/nexus-release-apply.XXXXXX)"
|
||||
trap cleanup EXIT
|
||||
run tar xzf "$TARBALL" -C "$APPLY_TMP"
|
||||
extracted="${APPLY_TMP}/nexus-release"
|
||||
validate_extracted_tree "$extracted"
|
||||
if [[ "$VALIDATE_ONLY" == 1 ]]; then
|
||||
info "Validate-only mode: tarball checksum and content validation passed"
|
||||
return 0
|
||||
fi
|
||||
backup_current
|
||||
sync_release "$extracted"
|
||||
rebuild_or_restart
|
||||
if [[ "$DRY_RUN" != 1 && "$SKIP_REBUILD" != 1 ]]; then
|
||||
verify_local_endpoints
|
||||
fi
|
||||
info "Release applied successfully"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# Fail if tracked shell scripts contain CR (CRLF) — breaks bash on Ubuntu.
|
||||
# Usage: bash deploy/check_shell_eol.sh
|
||||
# Run from repo root (or set NEXUS_DEPLOY_DIR).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_REPO_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)"
|
||||
ROOT="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "check_shell_eol: not a git repo at ${ROOT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BAD=0
|
||||
while IFS= read -r -d '' f; do
|
||||
if grep -q $'\r' "${f}" 2>/dev/null; then
|
||||
echo "CRLF/CR detected: ${f}" >&2
|
||||
BAD=1
|
||||
fi
|
||||
done < <(git ls-files -z '*.sh')
|
||||
|
||||
if [ "${BAD}" -ne 0 ]; then
|
||||
echo "Fix: git add --renormalize '*.sh' or run dos2unix on listed files." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "check_shell_eol: OK (all tracked *.sh are LF-only)"
|
||||
+29
-85
@@ -1,102 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — MySQL Auto-Backup Script
|
||||
# Usage: Add to crontab for daily 3AM backup
|
||||
# 0 3 * * * /path/to/nexus/deploy/db_backup.sh >> /var/log/nexus_backup.log 2>&1
|
||||
# Nexus — MySQL 定时备份(Docker 1Panel / 宿主机)
|
||||
#
|
||||
# Reads DATABASE_URL from .env for connection parameters.
|
||||
# Retains backups for 30 days, auto-deletes older ones.
|
||||
|
||||
# 0 3 * * * NEXUS_ROOT=/opt/nexus /opt/nexus/deploy/db_backup.sh >> /var/log/nexus_backup.log 2>&1
|
||||
#
|
||||
# 亦可通过: sudo bash deploy/install_ops_cron.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
ENV_FILE="$PROJECT_DIR/.env"
|
||||
BACKUP_DIR="$PROJECT_DIR/backups"
|
||||
RETENTION_DAYS=30
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-$(dirname "$SCRIPT_DIR")}"
|
||||
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/var/backups/nexus}"
|
||||
RETENTION_DAYS="${NEXUS_BACKUP_RETENTION_DAYS:-30}"
|
||||
DUMP_SH="${SCRIPT_DIR}/mysql_dump_to_file.sh"
|
||||
|
||||
# ── Load .env ──
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "[$(date)] ERROR: .env not found at $ENV_FILE"
|
||||
if [[ ! -f "$DUMP_SH" ]]; then
|
||||
echo "[$(date)] ERROR: missing $DUMP_SH"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$DUMP_SH" 2>/dev/null || true
|
||||
|
||||
# Parse DATABASE_URL from .env
|
||||
# Format: mysql+asyncmy://user:password@host:port/dbname
|
||||
DB_URL=$(grep -E '^NEXUS_DATABASE_URL=' "$ENV_FILE" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")
|
||||
if [ -z "$DB_URL" ]; then
|
||||
DB_URL=$(grep -E '^DATABASE_URL=' "$ENV_FILE" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")
|
||||
fi
|
||||
|
||||
if [ -z "$DB_URL" ]; then
|
||||
echo "[$(date)] ERROR: DATABASE_URL not found in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse mysql URL: mysql+asyncmy://user:pass@host:port/dbname → user pass host port dbname
|
||||
# Strip scheme
|
||||
CONN="${DB_URL#*://}"
|
||||
# Extract user:pass
|
||||
USERPASS="${CONN%%@*}"
|
||||
DB_USER="${USERPASS%%:*}"
|
||||
DB_PASS="${USERPASS#*:}"
|
||||
# Extract host:port/dbname
|
||||
HOSTDB="${CONN#*@}"
|
||||
DB_HOST="${HOSTDB%%:*}"
|
||||
PORTDB="${HOSTDB#*:}"
|
||||
DB_PORT="${PORTDB%%/*}"
|
||||
DB_NAME="${PORTDB#*/}"
|
||||
# Remove query params
|
||||
DB_NAME="${DB_NAME%%\?*}"
|
||||
|
||||
if [ -z "$DB_NAME" ] || [ -z "$DB_HOST" ] || [ -z "$DB_USER" ]; then
|
||||
echo "[$(date)] ERROR: Failed to parse DATABASE_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Create backup directory ──
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
RAW_FILE="${BACKUP_DIR}/nexus_daily_${TIMESTAMP}.sql"
|
||||
BACKUP_FILE="${RAW_FILE}.gz"
|
||||
|
||||
# ── Backup filename ──
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="$BACKUP_DIR/nexus_${DB_NAME}_${TIMESTAMP}.sql.gz"
|
||||
|
||||
# ── Run mysqldump ──
|
||||
echo "[$(date)] Starting backup: $DB_NAME @ $DB_HOST"
|
||||
|
||||
if [ -n "$DB_PASS" ]; then
|
||||
MYSQL_PWD="$DB_PASS" mysqldump \
|
||||
-h "$DB_HOST" \
|
||||
-P "${DB_PORT:-3306}" \
|
||||
-u "$DB_USER" \
|
||||
--single-transaction \
|
||||
--routines \
|
||||
--triggers \
|
||||
--set-gtid-purged=OFF \
|
||||
"$DB_NAME" | gzip > "$BACKUP_FILE"
|
||||
else
|
||||
mysqldump \
|
||||
-h "$DB_HOST" \
|
||||
-P "${DB_PORT:-3306}" \
|
||||
-u "$DB_USER" \
|
||||
--single-transaction \
|
||||
--routines \
|
||||
--triggers \
|
||||
--set-gtid-purged=OFF \
|
||||
"$DB_NAME" | gzip > "$BACKUP_FILE"
|
||||
fi
|
||||
|
||||
# ── Verify backup ──
|
||||
if [ -f "$BACKUP_FILE" ]; then
|
||||
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||
echo "[$(date)] Backup completed: $BACKUP_FILE ($SIZE)"
|
||||
else
|
||||
echo "[$(date)] ERROR: Backup file not created!"
|
||||
echo "[$(date)] Starting backup → $BACKUP_FILE"
|
||||
if ! NEXUS_ROOT="$NEXUS_ROOT" bash "$DUMP_SH" --output "$RAW_FILE" --root "$NEXUS_ROOT"; then
|
||||
echo "[$(date)] ERROR: mysqldump failed"
|
||||
rm -f "$RAW_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Cleanup old backups (retain 30 days) ──
|
||||
DELETED=$(find "$BACKUP_DIR" -name "nexus_*.sql.gz" -mtime +${RETENTION_DAYS} -delete -print | wc -l)
|
||||
if [ "$DELETED" -gt 0 ]; then
|
||||
gzip -f "$RAW_FILE"
|
||||
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||
echo "[$(date)] ERROR: backup file not created"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SIZE="$(du -h "$BACKUP_FILE" | cut -f1)"
|
||||
echo "[$(date)] Backup completed: $BACKUP_FILE ($SIZE)"
|
||||
|
||||
DELETED="$(find "$BACKUP_DIR" -name 'nexus_*.sql.gz' -mtime +"${RETENTION_DAYS}" -delete -print 2>/dev/null | wc -l)"
|
||||
if [[ "$DELETED" -gt 0 ]]; then
|
||||
echo "[$(date)] Cleaned up $DELETED old backup(s) (>${RETENTION_DAYS} days)"
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy-frontend.sh — Build frontend v1 + app-v2 and deploy to server
|
||||
#
|
||||
# Docker 生产:前端在镜像内构建,远程执行 nexus-1panel.sh upgrade
|
||||
# Supervisor:tar 上传到 web/app + web/app-v2 + supervisorctl restart
|
||||
#
|
||||
# Usage: bash deploy/deploy-frontend.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
|
||||
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
|
||||
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
|
||||
fi
|
||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||
|
||||
echo "═══ Nexus Frontend Deploy ═══"
|
||||
|
||||
REMOTE_RUNTIME=$(ssh_cmd 'bash -s' <<'REMOTE'
|
||||
set -euo pipefail
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker "$@" 2>/dev/null
|
||||
}
|
||||
compose_cmd() {
|
||||
if docker compose "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker compose "$@" 2>/dev/null
|
||||
}
|
||||
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
|
||||
[[ -d "$d/server" ]] || continue
|
||||
if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then
|
||||
prof="$d/docker/.host-profile"
|
||||
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
|
||||
if [[ -f "$prof" ]]; then
|
||||
args=(-f "$d/docker/docker-compose.prod.yml")
|
||||
docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \
|
||||
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
|
||||
args+=(-f "$d/docker/docker-compose.1panel.yml")
|
||||
q=$(cd "$d" && compose_cmd "${args[@]}" \
|
||||
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
|
||||
[[ -n "$q" ]] && echo "docker $d" && exit 0
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$d/deploy/nexus-1panel.sh" ]]; then
|
||||
q=$(docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E '^nexus-prod-nexus-' || true)
|
||||
[[ -n "$q" ]] && echo "docker $d" && exit 0
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo "supervisor $d"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
echo "unknown"
|
||||
REMOTE
|
||||
)
|
||||
|
||||
RUNTIME="${REMOTE_RUNTIME%% *}"
|
||||
DEPLOY_PATH="${REMOTE_RUNTIME#* }"
|
||||
[[ "$DEPLOY_PATH" == "$REMOTE_RUNTIME" ]] && DEPLOY_PATH="/www/wwwroot/api.synaglobal.vip"
|
||||
|
||||
echo "Remote runtime: ${RUNTIME} @ ${DEPLOY_PATH}"
|
||||
|
||||
echo "▶ Building frontend v1 (local verify)..."
|
||||
cd frontend && npx vite build && cd ..
|
||||
if [[ -d frontend-v2 ]]; then
|
||||
echo "▶ Building frontend v2 (/app-v2, local verify)..."
|
||||
(cd frontend-v2 && npm run build:safe)
|
||||
test -f web/app-v2/index.html
|
||||
fi
|
||||
echo "✓ Build complete"
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Docker 生产:重建镜像以包含新前端(非 tar 热更新)..."
|
||||
ssh_cmd "cd ${DEPLOY_PATH} && sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
|
||||
else
|
||||
echo "▶ Packaging web/app and web/app-v2..."
|
||||
pkg_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${pkg_dir}"' EXIT
|
||||
mkdir -p "${pkg_dir}/app"
|
||||
cp -a web/app/index.html web/app/assets "${pkg_dir}/app/"
|
||||
if [[ -f web/app-v2/index.html && -d web/app-v2/assets ]]; then
|
||||
mkdir -p "${pkg_dir}/app-v2"
|
||||
cp -a web/app-v2/index.html web/app-v2/assets "${pkg_dir}/app-v2/"
|
||||
fi
|
||||
tar czf /tmp/nexus-frontend.tar.gz -C "${pkg_dir}" .
|
||||
echo "✓ Package ready ($(du -h /tmp/nexus-frontend.tar.gz | cut -f1))"
|
||||
|
||||
echo "▶ Uploading to server..."
|
||||
scp "${SSH_OPTS[@]}" /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
|
||||
echo "✓ Upload complete"
|
||||
|
||||
echo "▶ Deploying on server..."
|
||||
ssh_cmd "set -e; mkdir -p ${DEPLOY_PATH}/web; tar xzf /tmp/nexus-frontend.tar.gz -C ${DEPLOY_PATH}/web; rm /tmp/nexus-frontend.tar.gz"
|
||||
echo "▶ Pruning orphaned assets..."
|
||||
if [[ -f deploy/prune_frontend_assets.py ]]; then
|
||||
if ! ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app --dry-run"; then
|
||||
echo "✗ Prune dry-run failed"
|
||||
exit 1
|
||||
fi
|
||||
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
|
||||
fi
|
||||
echo "✓ Extracted and pruned"
|
||||
|
||||
echo "▶ Restarting backend..."
|
||||
ssh_cmd "supervisorctl restart nexus"
|
||||
echo "✓ Restarted"
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:8600/health" 2>/dev/null || echo "000")
|
||||
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app/" 2>/dev/null || echo "000")
|
||||
SPA_V2=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8600/app-v2/" 2>/dev/null || echo "000")
|
||||
|
||||
echo ""
|
||||
echo "═══ Verification ═══"
|
||||
echo " /health → $HEALTH"
|
||||
echo " /app/ → $SPA"
|
||||
echo " /app-v2/ → $SPA_V2"
|
||||
|
||||
if [[ "$HEALTH" == "200" || "$HEALTH" == "ok" ]] && [[ "$SPA" == "200" ]] && [[ "$SPA_V2" == "200" ]]; then
|
||||
echo "✓ Deploy successful!"
|
||||
else
|
||||
echo "✗ Verification failed — check server logs"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run ON production server — 自动识别 Docker / Supervisor
|
||||
#
|
||||
# Usage (on server):
|
||||
# cd /opt/nexus && sudo bash deploy/deploy-on-server.sh
|
||||
# cd /www/wwwroot/api.synaglobal.vip && bash deploy/deploy-on-server.sh
|
||||
#
|
||||
# Optional env:
|
||||
# NEXUS_GITEA_TOKEN — if git pull needs auth
|
||||
# NEXUS_GIT_REMOTE — override authenticated remote, default: https://axs.kuma1xn.vip/admin/Nexus.git
|
||||
# SKIP_FRONTEND=1 — Supervisor 模式跳过热更新前端 tar
|
||||
# NEXUS_DEPLOY_PATH — 覆盖自动检测路径
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./detect_deploy_runtime.sh
|
||||
source "${SCRIPT_DIR}/detect_deploy_runtime.sh"
|
||||
|
||||
DEPLOY_PATH="$(resolve_nexus_root "${NEXUS_DEPLOY_PATH:-}" 2>/dev/null || true)"
|
||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
||||
echo "ERROR: cannot resolve Nexus root" >&2
|
||||
exit 1
|
||||
fi
|
||||
cd "${DEPLOY_PATH}"
|
||||
|
||||
RUNTIME="$(detect_nexus_runtime "$DEPLOY_PATH")"
|
||||
PORT="$(nexus_publish_port_for "$DEPLOY_PATH")"
|
||||
|
||||
echo "═══ Nexus server-side deploy ═══"
|
||||
echo "Path: ${DEPLOY_PATH}"
|
||||
echo "Runtime: ${RUNTIME}"
|
||||
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
GIT_REMOTE="${NEXUS_GIT_REMOTE:-https://axs.kuma1xn.vip/admin/Nexus.git}"
|
||||
GIT_REMOTE_AUTH="${GIT_REMOTE/https:\/\//https://admin:${NEXUS_GITEA_TOKEN}@}"
|
||||
git remote set-url origin "${GIT_REMOTE_AUTH}"
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Docker upgrade (pull + rebuild)..."
|
||||
exec env NEXUS_ROOT="$DEPLOY_PATH" bash "$DEPLOY_PATH/deploy/nexus-1panel.sh" upgrade
|
||||
fi
|
||||
|
||||
echo "▶ git fetch + reset --hard origin/main..."
|
||||
git fetch --all
|
||||
git reset --hard origin/main
|
||||
echo " HEAD: $(git log -1 --oneline)"
|
||||
|
||||
echo "▶ supervisorctl restart nexus..."
|
||||
supervisorctl restart nexus
|
||||
|
||||
if [[ -f /tmp/nexus-frontend.tar.gz && "${SKIP_FRONTEND:-0}" != "1" ]]; then
|
||||
echo "▶ extract /tmp/nexus-frontend.tar.gz..."
|
||||
mkdir -p web/app web/app-v2
|
||||
if tar tzf /tmp/nexus-frontend.tar.gz | grep -Eq '^(\./)?app/'; then
|
||||
tar xzf /tmp/nexus-frontend.tar.gz -C web
|
||||
else
|
||||
# Backward compatibility for legacy archives containing index.html + assets/ at root.
|
||||
tar xzf /tmp/nexus-frontend.tar.gz -C web/app --exclude=app-v2
|
||||
if tar tzf /tmp/nexus-frontend.tar.gz | grep -Eq '^(\./)?app-v2/'; then
|
||||
tar xzf /tmp/nexus-frontend.tar.gz -C web/app-v2 --strip-components=1 app-v2
|
||||
fi
|
||||
fi
|
||||
rm -f /tmp/nexus-frontend.tar.gz
|
||||
if [[ -f deploy/prune_frontend_assets.py ]]; then
|
||||
python3 deploy/prune_frontend_assets.py web/app --dry-run
|
||||
python3 deploy/prune_frontend_assets.py web/app
|
||||
fi
|
||||
supervisorctl restart nexus
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
HEALTH=$(curl -s "http://127.0.0.1:${PORT}/health" || true)
|
||||
SPA=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/app/" || echo "000")
|
||||
SPA_V2=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/app-v2/" || echo "000")
|
||||
echo " /health → ${HEALTH}"
|
||||
echo " /app/ → ${SPA}"
|
||||
echo " /app-v2/ → ${SPA_V2}"
|
||||
|
||||
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" && "${SPA_V2}" == "200" ]]; then
|
||||
echo "✓ Server deploy OK"
|
||||
suggest_ops_cron "$DEPLOY_PATH"
|
||||
else
|
||||
echo "✗ Verify failed — check: supervisorctl status nexus" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
# Full production deploy: local rsync + backend upgrade + frontend + health
|
||||
#
|
||||
# 默认从本机工作区 rsync 到生产机,不 push Gitea、不远程 git pull。
|
||||
#
|
||||
# Prerequisites on YOUR machine:
|
||||
# - rsync, SSH Host nexus OR export NEXUS_SSH="azureuser@20.24.218.235"
|
||||
# - Key: export NEXUS_SSH_KEY=~/.ssh/id_rsa_nexus
|
||||
#
|
||||
# Usage:
|
||||
# cd "$NEXUS_ROOT"
|
||||
# bash deploy/pre_deploy_check.sh
|
||||
# bash deploy/deploy-production.sh
|
||||
#
|
||||
# 仍走 Gitea(仅当显式设置):
|
||||
# NEXUS_DEPLOY_VIA_GIT=1 bash deploy/deploy-production.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT}"
|
||||
|
||||
SECRETS="${ROOT}/deploy/nexus-1panel.secrets.sh"
|
||||
if [[ -f "$SECRETS" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$SECRETS"
|
||||
fi
|
||||
|
||||
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
|
||||
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
|
||||
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
|
||||
fi
|
||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||
scp_cmd() { scp "${SSH_OPTS[@]}" "$@"; }
|
||||
|
||||
DEPLOY_VIA_GIT="${NEXUS_DEPLOY_VIA_GIT:-0}"
|
||||
case "${DEPLOY_VIA_GIT}" in
|
||||
1|true|yes|on) DEPLOY_VIA_GIT=1 ;;
|
||||
*) DEPLOY_VIA_GIT=0 ;;
|
||||
esac
|
||||
|
||||
echo "═══ Nexus production deploy ═══"
|
||||
echo "SSH target: ${NEXUS_SSH_HOST}"
|
||||
echo "Mode: $([[ "$DEPLOY_VIA_GIT" == 1 ]] && echo 'Gitea git pull' || echo 'local rsync')"
|
||||
|
||||
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
||||
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
||||
echo " Configure ~/.ssh/config (Host nexus) or set NEXUS_SSH / NEXUS_SSH_KEY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REMOTE_DETECT=$(ssh_cmd 'bash -s' <<'REMOTE'
|
||||
set -euo pipefail
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker "$@" 2>/dev/null
|
||||
}
|
||||
compose_cmd() {
|
||||
if docker compose "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker compose "$@" 2>/dev/null
|
||||
}
|
||||
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
|
||||
[[ -d "$d/server" ]] || continue
|
||||
echo "ROOT=$d"
|
||||
if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then
|
||||
prof="$d/docker/.host-profile"
|
||||
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
|
||||
if [[ -f "$prof" ]]; then
|
||||
args=(-f "$d/docker/docker-compose.prod.yml")
|
||||
docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \
|
||||
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
|
||||
args+=(-f "$d/docker/docker-compose.1panel.yml")
|
||||
q=$(cd "$d" && compose_cmd "${args[@]}" \
|
||||
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
|
||||
[[ -n "$q" ]] && echo RUNTIME=docker && exit 0
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$d/deploy/nexus-1panel.sh" ]] && command -v nx >/dev/null 2>&1; then
|
||||
q=$(docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E '^nexus-prod-nexus-' || true)
|
||||
[[ -n "$q" ]] && echo RUNTIME=docker && exit 0
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo RUNTIME=supervisor
|
||||
exit 0
|
||||
fi
|
||||
echo RUNTIME=unknown
|
||||
exit 0
|
||||
done
|
||||
echo RUNTIME=none
|
||||
REMOTE
|
||||
)
|
||||
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-$(echo "$REMOTE_DETECT" | sed -n 's/^ROOT=//p' | head -1)}"
|
||||
RUNTIME="$(echo "$REMOTE_DETECT" | sed -n 's/^RUNTIME=//p' | head -1)"
|
||||
|
||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
||||
fi
|
||||
if [[ -z "$RUNTIME" || "$RUNTIME" == "none" ]]; then
|
||||
RUNTIME="unknown"
|
||||
fi
|
||||
|
||||
echo "Deploy path: ${DEPLOY_PATH}"
|
||||
echo "Runtime: ${RUNTIME}"
|
||||
|
||||
resolve_deploy_chown() {
|
||||
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
|
||||
echo "${NEXUS_DEPLOY_CHOWN}"
|
||||
return
|
||||
fi
|
||||
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
|
||||
echo "www:www"
|
||||
return
|
||||
fi
|
||||
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
|
||||
}
|
||||
DEPLOY_CHOWN="$(resolve_deploy_chown)"
|
||||
echo "File owner: ${DEPLOY_CHOWN}"
|
||||
|
||||
if [[ "$DEPLOY_VIA_GIT" == 1 ]]; then
|
||||
echo "▶ Git mode: remote git pull..."
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
ssh_cmd "cd ${DEPLOY_PATH} && sudo git fetch --all && sudo git reset --hard origin/main && \
|
||||
sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
|
||||
elif [[ "$RUNTIME" == "supervisor" ]]; then
|
||||
ssh_cmd "cd ${DEPLOY_PATH} && git fetch --all && git reset --hard origin/main && supervisorctl restart nexus"
|
||||
else
|
||||
echo "ERROR: 无法识别远程运行时" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "▶ Local sync → ${DEPLOY_PATH}..."
|
||||
NEXUS_DEPLOY_PATH="${DEPLOY_PATH}" bash "${ROOT}/deploy/rsync-local-to-server.sh"
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Docker: 重建镜像(--skip-git)..."
|
||||
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade --skip-git"
|
||||
elif [[ "$RUNTIME" == "supervisor" ]]; then
|
||||
echo "▶ Supervisor: restart..."
|
||||
ssh_cmd "supervisorctl restart nexus"
|
||||
else
|
||||
echo "ERROR: 无法识别远程运行时(非 Docker 也非 Supervisor)" >&2
|
||||
echo " 请手动: ssh ${NEXUS_SSH_HOST} 'sudo nx update'" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME" == "docker" || "$RUNTIME" == "supervisor" ]]; then
|
||||
echo "▶ Frontend: local vite build + upload to host web/app and web/app-v2..."
|
||||
cd frontend && npx vite build && cd ..
|
||||
if [[ -d frontend-v2 ]]; then
|
||||
(cd frontend-v2 && npm run build:safe)
|
||||
test -f web/app-v2/index.html
|
||||
fi
|
||||
pkg_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${pkg_dir}"' EXIT
|
||||
mkdir -p "${pkg_dir}/app"
|
||||
cp -a web/app/index.html web/app/assets "${pkg_dir}/app/"
|
||||
if [[ -f web/app-v2/index.html && -d web/app-v2/assets ]]; then
|
||||
mkdir -p "${pkg_dir}/app-v2"
|
||||
cp -a web/app-v2/index.html web/app-v2/assets "${pkg_dir}/app-v2/"
|
||||
fi
|
||||
tar czf /tmp/nexus-frontend.tar.gz -C "${pkg_dir}" .
|
||||
scp_cmd /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
|
||||
ssh_cmd "set -e; sudo mkdir -p ${DEPLOY_PATH}/web; sudo tar xzf /tmp/nexus-frontend.tar.gz -C ${DEPLOY_PATH}/web; rm /tmp/nexus-frontend.tar.gz; sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app ${DEPLOY_PATH}/web/app-v2"
|
||||
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
|
||||
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app --dry-run"
|
||||
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
echo "▶ Sync host web/app and web/app-v2 into container..."
|
||||
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} NEXUS_PUBLISH_PORT=\$(grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod 2>/dev/null | cut -d= -f2 | tr -d '\"' || echo 8600) bash ${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" || {
|
||||
echo "WARN: web/app + web/app-v2 sync failed — run sync_webapp_to_container.sh on server manually" >&2
|
||||
}
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
PORT="$(ssh_cmd "grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod 2>/dev/null | cut -d= -f2 | tr -d '\"'" 2>/dev/null || true)"
|
||||
PORT="${PORT:-8600}"
|
||||
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:${PORT}/health" 2>/dev/null || true)
|
||||
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app/" 2>/dev/null || echo "000")
|
||||
SPA_V2=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app-v2/" 2>/dev/null || echo "000")
|
||||
echo " /health → ${HEALTH}"
|
||||
echo " /app/ → ${SPA}"
|
||||
echo " /app-v2/ → ${SPA_V2}"
|
||||
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" && "${SPA_V2}" == "200" ]]; then
|
||||
echo "✓ Deploy successful"
|
||||
if ssh_cmd "command -v crontab >/dev/null && crontab -l 2>/dev/null | grep -q nexus-health-monitor" 2>/dev/null; then
|
||||
echo "✓ Ops cron (health_monitor) already installed"
|
||||
else
|
||||
echo "▶ Installing ops cron (health_monitor + db_backup)…"
|
||||
if ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/install_ops_cron.sh" 2>/dev/null; then
|
||||
echo "✓ Ops cron installed"
|
||||
else
|
||||
echo " 提示: 远程 cron 安装失败,可在服务器执行: sudo nx cron"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "✗ Verification failed — check logs on server" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# Detect 1Panel App Store MySQL/Redis container names on 1panel-network.
|
||||
# See: https://github.com/1Panel-dev/1Panel/issues/11676
|
||||
# https://github.com/1Panel-dev/appstore (mysql/redis → networks: 1panel-network)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
network_container_names() {
|
||||
docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null \
|
||||
| tr ' ' '\n' | sed '/^$/d'
|
||||
}
|
||||
|
||||
pick_service_container() {
|
||||
local kind="$1"
|
||||
local names line
|
||||
names="$(network_container_names)"
|
||||
if [[ -z "$names" ]]; then
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -iE "(^1Panel-${kind}-|${kind})" | head -1 || true
|
||||
return
|
||||
fi
|
||||
line="$(echo "$names" | grep -iE "^1Panel-${kind}-" | head -1 || true)"
|
||||
if [[ -n "$line" ]]; then
|
||||
echo "$line"
|
||||
return
|
||||
fi
|
||||
echo "$names" | grep -i "$kind" | head -1 || true
|
||||
}
|
||||
|
||||
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
echo "1panel-network not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MYSQL_HOST="$(pick_service_container mysql)"
|
||||
REDIS_HOST="$(pick_service_container redis)"
|
||||
|
||||
if [[ -z "$MYSQL_HOST" && -z "$REDIS_HOST" ]]; then
|
||||
echo "No MySQL/Redis containers on 1panel-network" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
[[ -n "$MYSQL_HOST" ]] && echo "NEXUS_1PANEL_DB_HOST=${MYSQL_HOST}"
|
||||
[[ -n "$REDIS_HOST" ]] && echo "NEXUS_1PANEL_REDIS_HOST=${REDIS_HOST}"
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — 部署运行时检测(Docker 1Panel / Supervisor 裸机)
|
||||
#
|
||||
# source deploy/detect_deploy_runtime.sh
|
||||
# ROOT="$(resolve_nexus_root)"
|
||||
# RUNTIME="$(detect_nexus_runtime "$ROOT")" # docker | supervisor | unknown
|
||||
set -euo pipefail
|
||||
|
||||
_COMPOSE_FILE="docker/docker-compose.prod.yml"
|
||||
_COMPOSE_1PANEL="docker/docker-compose.1panel.yml"
|
||||
_ENV_PROD="docker/.env.prod"
|
||||
|
||||
resolve_nexus_root() {
|
||||
local hint="${1:-}"
|
||||
if [[ -n "$hint" && -d "$hint/server" ]]; then
|
||||
echo "$hint"
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${NEXUS_ROOT:-}" && -d "${NEXUS_ROOT}/server" ]]; then
|
||||
echo "$NEXUS_ROOT"
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${NEXUS_DEPLOY_PATH:-}" && -d "${NEXUS_DEPLOY_PATH}/server" ]]; then
|
||||
echo "$NEXUS_DEPLOY_PATH"
|
||||
return 0
|
||||
fi
|
||||
local d
|
||||
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
|
||||
if [[ -d "$d/server" ]]; then
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
for d in /www/wwwroot/*/; do
|
||||
[[ -d "${d}server" ]] || continue
|
||||
echo "${d%/}"
|
||||
return 0
|
||||
done
|
||||
if command -v supervisorctl >/dev/null 2>&1; then
|
||||
local cwd
|
||||
cwd="$(supervisorctl status nexus 2>/dev/null | grep -oP 'cwd=\K[^ ,]+' || true)"
|
||||
if [[ -n "$cwd" && -d "$cwd/server" ]]; then
|
||||
echo "$cwd"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
_has_1panel_network() {
|
||||
docker network inspect 1panel-network >/dev/null 2>&1
|
||||
}
|
||||
|
||||
detect_nexus_runtime() {
|
||||
local root="$1"
|
||||
if [[ -f "$root/$_ENV_PROD" ]] && command -v docker >/dev/null 2>&1 \
|
||||
&& docker compose version >/dev/null 2>&1; then
|
||||
local -a args=(-f "$root/$_COMPOSE_FILE")
|
||||
if _has_1panel_network && [[ -f "$root/$_COMPOSE_1PANEL" ]]; then
|
||||
args+=(-f "$root/$_COMPOSE_1PANEL")
|
||||
fi
|
||||
local prof q
|
||||
prof="$root/docker/.host-profile"
|
||||
[[ -f "$prof" ]] || prof="$root/docker/profiles/2c8g.env"
|
||||
if [[ -f "$prof" ]]; then
|
||||
q="$(cd "$root" && docker compose "${args[@]}" \
|
||||
--env-file "$root/$_ENV_PROD" --env-file "$prof" \
|
||||
ps -q nexus 2>/dev/null || true)"
|
||||
if [[ -n "$q" ]]; then
|
||||
echo docker
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 \
|
||||
&& supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo supervisor
|
||||
return 0
|
||||
fi
|
||||
echo unknown
|
||||
}
|
||||
|
||||
nexus_publish_port_for() {
|
||||
local root="$1"
|
||||
local port
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$root/$_ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
if [[ -z "$port" && -f "$root/.env" ]]; then
|
||||
port="$(grep -E '^NEXUS_PORT=' "$root/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
fi
|
||||
echo "${port:-8600}"
|
||||
}
|
||||
|
||||
nexus_restart_service() {
|
||||
local root="$1"
|
||||
local runtime
|
||||
runtime="$(detect_nexus_runtime "$root")"
|
||||
case "$runtime" in
|
||||
docker)
|
||||
local cname
|
||||
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -n "$cname" ]]; then
|
||||
docker restart "$cname"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
;;
|
||||
supervisor)
|
||||
supervisorctl restart nexus
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
nexus_has_ops_cron() {
|
||||
command -v crontab >/dev/null 2>&1 \
|
||||
&& crontab -l 2>/dev/null | grep -q 'nexus-health-monitor'
|
||||
}
|
||||
|
||||
suggest_ops_cron() {
|
||||
local root="$1"
|
||||
if nexus_has_ops_cron; then
|
||||
return 0
|
||||
fi
|
||||
echo ""
|
||||
echo "[WARN] 未检测到 health_monitor / db_backup cron"
|
||||
echo " 建议: sudo bash ${root}/deploy/install_ops_cron.sh"
|
||||
echo " 或: sudo nx cron"
|
||||
}
|
||||
|
||||
prompt_ops_cron_if_missing() {
|
||||
local root="$1"
|
||||
if nexus_has_ops_cron; then
|
||||
echo "[INFO] Crontab 已含 health_monitor / db_backup"
|
||||
return 0
|
||||
fi
|
||||
suggest_ops_cron "$root"
|
||||
if [[ ! -t 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
local ans
|
||||
read -r -p "现在安装巡检/备份 cron? [Y/n]: " ans
|
||||
if [[ "$ans" == "n" || "$ans" == "N" ]]; then
|
||||
echo "[WARN] 已跳过 cron 安装"
|
||||
return 0
|
||||
fi
|
||||
bash "${root}/deploy/install_ops_cron.sh"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# 下载并执行 install-nexus-fresh.sh(公共仓库无需令牌)
|
||||
#
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/download-install-fresh.sh" | bash
|
||||
#
|
||||
# 推荐直接使用:
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
|
||||
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
OUT="${OUT:-/tmp/install-nexus-fresh.sh}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
|
||||
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
|
||||
done
|
||||
|
||||
download() {
|
||||
local url base
|
||||
for base in "${NEXUS_RAW_BASE}" "${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/install-nexus-fresh.sh"
|
||||
if curl -fsSL "$url" -o "$OUT" 2>/dev/null && head -1 "$OUT" | grep -q '^#!/'; then
|
||||
ok "已下载 install-nexus-fresh.sh"
|
||||
chmod +x "$OUT"
|
||||
exec bash "$OUT" "$@"
|
||||
fi
|
||||
done
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
url="${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/api/v1/repos/admin/Nexus/raw/deploy/install-nexus-fresh.sh?ref=${NEXUS_GIT_BRANCH}"
|
||||
if curl -fsSL -H "Authorization: token ${NEXUS_GITEA_TOKEN}" "$url" -o "$OUT" 2>/dev/null \
|
||||
&& head -1 "$OUT" | grep -q '^#!/'; then
|
||||
ok "已下载(API + 令牌)"
|
||||
chmod +x "$OUT"
|
||||
exec bash "$OUT" "$@"
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! download "$@"; then
|
||||
err "下载失败,请改用公共仓库一键入口:"
|
||||
err " curl -fsSL \"${NEXUS_RAW_BASE}/deploy/quick-install.sh\" | bash"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Grant nexus@'%' on 1Panel App Store MySQL for Docker (1panel-network) install wizard.
|
||||
# Fixes: Access denied for user 'nexus'@'172.18.x.x' (1045)
|
||||
#
|
||||
# Usage:
|
||||
# MYSQL_ROOT_PASSWORD='root密码' NEXUS_DB_PASSWORD='nexus密码' bash deploy/fix-1panel-mysql-grant.sh
|
||||
# bash deploy/fix-1panel-mysql-grant.sh # prompts
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_DB_NAME="${NEXUS_DB_NAME:-nexus}"
|
||||
NEXUS_DB_USER="${NEXUS_DB_USER:-nexus}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; exit 1; }
|
||||
|
||||
pick_mysql_container() {
|
||||
local name
|
||||
if [[ -n "${MYSQL_CONTAINER:-}" ]]; then
|
||||
echo "$MYSQL_CONTAINER"
|
||||
return 0
|
||||
fi
|
||||
if docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
name="$(docker network inspect 1panel-network --format '{{range .Containers}}{{.Name}} {{end}}' \
|
||||
| tr ' ' '\n' | grep -iE '^1Panel-mysql-' | head -1 || true)"
|
||||
[[ -n "$name" ]] && echo "$name" && return 0
|
||||
fi
|
||||
name="$(docker ps --format '{{.Names}}' | grep -iE '^1Panel-mysql-' | head -1 || true)"
|
||||
[[ -n "$name" ]] && echo "$name" && return 0
|
||||
error "未找到 1Panel MySQL 容器(可设 MYSQL_CONTAINER=名称)"
|
||||
}
|
||||
|
||||
prompt_secret() {
|
||||
local var="$1" prompt="$2"
|
||||
if [[ -n "${!var:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
read -r -s -p "$prompt: " "$var"
|
||||
echo ""
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
error "$var 不能为空"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
[[ "${EUID:-$(id -u)}" -eq 0 ]] || error "请用 root 运行"
|
||||
|
||||
local mysql_c
|
||||
mysql_c="$(pick_mysql_container)"
|
||||
info "MySQL 容器: $mysql_c"
|
||||
|
||||
prompt_secret MYSQL_ROOT_PASSWORD "1Panel MySQL root 密码"
|
||||
prompt_secret NEXUS_DB_PASSWORD "nexus 用户密码(与安装向导步骤 3 一致)"
|
||||
|
||||
info "创建库/用户并授权 ${NEXUS_DB_USER}@'%' → ${NEXUS_DB_NAME}.* ..."
|
||||
|
||||
docker exec -i "$mysql_c" mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" <<SQL
|
||||
CREATE DATABASE IF NOT EXISTS \`${NEXUS_DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER IF NOT EXISTS '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
|
||||
ALTER USER '${NEXUS_DB_USER}'@'%' IDENTIFIED BY '${NEXUS_DB_PASSWORD}';
|
||||
GRANT ALL PRIVILEGES ON \`${NEXUS_DB_NAME}\`.* TO '${NEXUS_DB_USER}'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
SELECT user, host FROM mysql.user WHERE user = '${NEXUS_DB_USER}';
|
||||
SQL
|
||||
|
||||
info "完成。请在安装向导步骤 3 使用: 库/用户=${NEXUS_DB_NAME}/${NEXUS_DB_USER},密码与上面一致"
|
||||
info "若已改 install.py,执行: bash deploy/sync-install-wizard-to-container.sh"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Gate Log — 门控执行日志记录器
|
||||
|
||||
每次门控检查的结果都追加到 deploy/gate_log.jsonl (JSON Lines)
|
||||
每行一条记录,包含:时间戳、门控名、结果、详情
|
||||
|
||||
这个文件是用户审查AI是否偷懒的关键证据:
|
||||
- 查看最近一次: tail -1 deploy/gate_log.jsonl | python -m json.tool
|
||||
- 查看所有BLOCK: grep '"BLOCK"' deploy/gate_log.jsonl
|
||||
- 查看某天: grep '2026-05-26' deploy/gate_log.jsonl
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
GATE_LOG = os.path.join(
|
||||
os.environ.get("NEXUS_DEPLOY_DIR", "/opt/nexus"),
|
||||
"deploy", "gate_log.jsonl"
|
||||
)
|
||||
|
||||
def log_gate(gate_name: str, result: str, detail: str = "", files_checked: list = None):
|
||||
"""Record a gate check result.
|
||||
|
||||
Args:
|
||||
gate_name: e.g. "Changelog", "Audit", "Test", "Lint", "Import", "Security", "Review"
|
||||
result: "PASS", "BLOCK", "SKIP", "WARN"
|
||||
detail: Human-readable detail
|
||||
files_checked: List of files involved in this gate check
|
||||
"""
|
||||
entry = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"gate": gate_name,
|
||||
"result": result,
|
||||
"detail": detail,
|
||||
}
|
||||
if files_checked:
|
||||
entry["files"] = files_checked
|
||||
|
||||
os.makedirs(os.path.dirname(GATE_LOG), exist_ok=True)
|
||||
with open(GATE_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) >= 3:
|
||||
log_gate(sys.argv[1], sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else "")
|
||||
else:
|
||||
# Print recent log
|
||||
if os.path.exists(GATE_LOG):
|
||||
with open(GATE_LOG, encoding="utf-8") as f:
|
||||
lines = f.readlines()[-20:]
|
||||
for line in lines:
|
||||
d = json.loads(line)
|
||||
icon = {"PASS": "✅", "BLOCK": "🚫", "SKIP": "⏭️", "WARN": "⚠️"}.get(d["result"], "?")
|
||||
print(f"{d['ts'][:19]} {icon} {d['gate']}: {d.get('detail','')}")
|
||||
else:
|
||||
print("No gate log found")
|
||||
+317
-56
@@ -1,70 +1,331 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — External Health Monitor (Layer 3 of 3-tier guardian)
|
||||
# Runs via crontab every minute.
|
||||
# Does NOT depend on Python — even if Python completely crashes, this script works.
|
||||
# Runs via crontab every minute. Works on Docker (1Panel) and Supervisor bare-metal.
|
||||
#
|
||||
# Guardian layers:
|
||||
# Layer 1: Supervisor — auto-restart crashed Python process
|
||||
# Layer 2: Python self_monitor — internal Redis/MySQL/WS checks (30s)
|
||||
# Layer 3: Shell health_monitor.sh — external HTTP health check (1min cron)
|
||||
#
|
||||
# Flow:
|
||||
# 1. HTTP GET http://127.0.0.1:8600/health
|
||||
# 2. If fails >= 3 consecutive times → restart via supervisorctl + Telegram alert
|
||||
# 3. If restart succeeds → Telegram recovery notification
|
||||
# 4. If restart fails → Telegram "needs human intervention" alert
|
||||
#
|
||||
# *** Configure DEPLOY_DIR and NEXUS_SERVICE before deploying ***
|
||||
# Layer 1: Docker restart policy / Supervisor autorestart
|
||||
# Layer 2: Python self_monitor (30s)
|
||||
# Layer 3: This script — HTTP /health, 3 failures → restart + optional Telegram
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-/opt/nexus}"
|
||||
NEXUS_SERVICE="${NEXUS_SERVICE:-nexus}"
|
||||
FAIL_FILE="/tmp/nexus_health_fail_count"
|
||||
STATE_DIR=""
|
||||
FAIL_FILE=""
|
||||
LAST_RESTART_FILE=""
|
||||
# Legacy paths (cron user may have created root-owned files under /tmp)
|
||||
LEGACY_FAIL_FILE="/tmp/nexus_health_fail_count"
|
||||
LEGACY_LAST_RESTART_FILE="/tmp/nexus_health_last_restart"
|
||||
MAX_FAIL=3
|
||||
HEALTH_URL="http://127.0.0.1:8600/health"
|
||||
RESTART_COOLDOWN_SEC="${NEXUS_HEALTH_RESTART_COOLDOWN:-300}"
|
||||
ENV_PROD="${DEPLOY_DIR}/docker/.env.prod"
|
||||
|
||||
# ── Read Telegram config from .env file ──
|
||||
DOTENV="$DEPLOY_DIR/.env"
|
||||
|
||||
BOT_TOKEN=""
|
||||
CHAT_ID=""
|
||||
|
||||
if [ -f "$DOTENV" ]; then
|
||||
BOT_TOKEN=$(grep '^NEXUS_TELEGRAM_BOT_TOKEN=' "$DOTENV" 2>/dev/null | cut -d'=' -f2- || echo "")
|
||||
CHAT_ID=$(grep '^NEXUS_TELEGRAM_CHAT_ID=' "$DOTENV" 2>/dev/null | cut -d'=' -f2- || echo "")
|
||||
fi
|
||||
|
||||
send_telegram() {
|
||||
[ -z "$BOT_TOKEN" ] || [ -z "$CHAT_ID" ] && return 1
|
||||
curl -sf --max-time 10 "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d chat_id="$CHAT_ID" \
|
||||
-d text="$1" \
|
||||
-d parse_mode=HTML \
|
||||
> /dev/null 2>&1
|
||||
ensure_state_paths() {
|
||||
if [[ -n "$STATE_DIR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local preferred="${DEPLOY_DIR}/var/layer3-health"
|
||||
local fallback="${HOME:-/tmp}/.nexus/layer3-health"
|
||||
if mkdir -p "$preferred" 2>/dev/null && [[ -w "$preferred" ]]; then
|
||||
STATE_DIR="$preferred"
|
||||
elif command -v sudo >/dev/null 2>&1 && sudo mkdir -p "$preferred" 2>/dev/null \
|
||||
&& sudo chown -R "$(id -un):$(id -gn)" "$preferred" 2>/dev/null \
|
||||
&& [[ -w "$preferred" ]]; then
|
||||
STATE_DIR="$preferred"
|
||||
else
|
||||
mkdir -p "$fallback"
|
||||
STATE_DIR="$fallback"
|
||||
fi
|
||||
FAIL_FILE="${STATE_DIR}/fail_count"
|
||||
LAST_RESTART_FILE="${STATE_DIR}/last_restart"
|
||||
}
|
||||
|
||||
# ── Health Check ──
|
||||
if ! curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
# Python is NOT responding
|
||||
FAIL=$(cat "$FAIL_FILE" 2>/dev/null || echo "0")
|
||||
FAIL=$((FAIL + 1))
|
||||
echo "$FAIL" > "$FAIL_FILE"
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo docker "$@" 2>/dev/null
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$FAIL" -ge "$MAX_FAIL" ]; then
|
||||
# Consecutive failures >= threshold → attempt restart
|
||||
send_telegram "🔴 <b>Nexus后端连续${MAX_FAIL}次无响应</b>\n正在通过Supervisor自动重启..."
|
||||
supervisorctl restart "$NEXUS_SERVICE" > /dev/null 2>&1
|
||||
sleep 5
|
||||
init_layer3_state() {
|
||||
ensure_state_paths
|
||||
chmod 755 "$STATE_DIR" 2>/dev/null || true
|
||||
if [[ ! -f "$FAIL_FILE" && -f "$LEGACY_FAIL_FILE" ]]; then
|
||||
cp "$LEGACY_FAIL_FILE" "$FAIL_FILE" 2>/dev/null || true
|
||||
fi
|
||||
if [[ ! -f "$LAST_RESTART_FILE" && -f "$LEGACY_LAST_RESTART_FILE" ]]; then
|
||||
cp "$LEGACY_LAST_RESTART_FILE" "$LAST_RESTART_FILE" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Verify restart
|
||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
send_telegram "🟢 <b>Nexus后端已恢复</b>\nSupervisor重启成功"
|
||||
echo "0" > "$FAIL_FILE"
|
||||
else
|
||||
send_telegram "🔴 <b>Nexus重启后仍未恢复!请人工介入</b>\nSupervisor重启失败,需要手动检查"
|
||||
# Don't reset fail count — keep retrying each cron cycle
|
||||
fi
|
||||
write_state_file() {
|
||||
local path="$1" value="$2"
|
||||
init_layer3_state
|
||||
printf '%s\n' "$value" >"$path" 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
resolve_publish_port() {
|
||||
local port
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
echo "${port:-8600}"
|
||||
}
|
||||
|
||||
PUBLISH_PORT="$(resolve_publish_port)"
|
||||
HEALTH_URL="http://127.0.0.1:${PUBLISH_PORT}/health"
|
||||
|
||||
resolve_nexus_container() {
|
||||
docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||||
}
|
||||
|
||||
read_env_var() {
|
||||
local key="$1" file="$2"
|
||||
grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"' || true
|
||||
}
|
||||
|
||||
normalize_env_value() {
|
||||
local v="${1:-}"
|
||||
v="${v//$'\r'/}"
|
||||
if [[ ${#v} -ge 2 && ${v:0:1} == '"' && ${v: -1} == '"' ]]; then
|
||||
v="${v:1:-1}"
|
||||
v="${v//\\n/$'\n'}"
|
||||
v="${v//\\\"/\"}"
|
||||
v="${v//\\\\/\\}"
|
||||
fi
|
||||
echo "$v"
|
||||
}
|
||||
|
||||
load_telegram_from_db() {
|
||||
local cname="$1"
|
||||
[[ -n "$cname" ]] || return 1
|
||||
[[ -n "${BOT_TOKEN:-}" && -n "${CHAT_ID:-}" ]] && return 0
|
||||
local out token chat
|
||||
out="$(docker_cmd exec "$cname" python3 -c "
|
||||
import asyncio
|
||||
from server.config import settings as s
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
|
||||
async def main():
|
||||
async with AsyncSessionLocal() as db:
|
||||
await s.load_settings_from_db(db)
|
||||
t = (s.TELEGRAM_BOT_TOKEN or '').strip()
|
||||
c = (s.TELEGRAM_CHAT_ID or '').strip()
|
||||
if t and c:
|
||||
print(t)
|
||||
print(c)
|
||||
|
||||
asyncio.run(main())
|
||||
" 2>/dev/null)" || true
|
||||
[[ -n "$out" ]] || return 1
|
||||
token="$(printf '%s\n' "$out" | sed -n '1p')"
|
||||
chat="$(printf '%s\n' "$out" | sed -n '2p')"
|
||||
[[ -n "$token" && -n "$chat" ]] || return 1
|
||||
BOT_TOKEN="$token"
|
||||
CHAT_ID="$chat"
|
||||
}
|
||||
|
||||
load_telegram_config() {
|
||||
local cname="$1" dotenv
|
||||
BOT_TOKEN=""
|
||||
CHAT_ID=""
|
||||
for dotenv in \
|
||||
"${DEPLOY_DIR}/.env" \
|
||||
"$ENV_PROD" \
|
||||
; do
|
||||
[[ -f "$dotenv" ]] || continue
|
||||
BOT_TOKEN="$(read_env_var NEXUS_TELEGRAM_BOT_TOKEN "$dotenv")"
|
||||
CHAT_ID="$(read_env_var NEXUS_TELEGRAM_CHAT_ID "$dotenv")"
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
|
||||
done
|
||||
if [[ -n "$cname" ]]; then
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
NEXUS_TELEGRAM_BOT_TOKEN=*) BOT_TOKEN="${line#NEXUS_TELEGRAM_BOT_TOKEN=}" ;;
|
||||
NEXUS_TELEGRAM_CHAT_ID=*) CHAT_ID="${line#NEXUS_TELEGRAM_CHAT_ID=}" ;;
|
||||
esac
|
||||
done < <(docker_cmd exec "$cname" grep -E '^NEXUS_TELEGRAM_' /app/.env 2>/dev/null || true)
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
|
||||
load_telegram_from_db "$cname" || true
|
||||
fi
|
||||
BOT_TOKEN="$(normalize_env_value "$BOT_TOKEN")"
|
||||
CHAT_ID="$(normalize_env_value "$CHAT_ID")"
|
||||
}
|
||||
|
||||
send_telegram() {
|
||||
local msg="${1}" resp http log="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||
BOT_TOKEN="$(normalize_env_value "${BOT_TOKEN:-}")"
|
||||
CHAT_ID="$(normalize_env_value "${CHAT_ID:-}")"
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] || return 1
|
||||
resp="$(curl -s --max-time 15 -w '\n%{http_code}' \
|
||||
"https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${CHAT_ID}" \
|
||||
--data-urlencode "text=${msg}" \
|
||||
-d "parse_mode=HTML" 2>/dev/null)" || return 1
|
||||
http="${resp##*$'\n'}"
|
||||
if [[ "$http" != "200" ]]; then
|
||||
echo "$(date -Is 2>/dev/null || date) telegram send failed http=${http}" >> "$log" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
bump_fail_count() {
|
||||
local fail=0
|
||||
init_layer3_state
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
(
|
||||
flock -x 9
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
fail=$((fail + 1))
|
||||
write_state_file "$FAIL_FILE" "$fail" || true
|
||||
) 9>"${FAIL_FILE}.lock"
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
else
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
fail=$((fail + 1))
|
||||
write_state_file "$FAIL_FILE" "$fail" || true
|
||||
fi
|
||||
echo "$fail"
|
||||
}
|
||||
|
||||
reset_fail_count() {
|
||||
write_state_file "$FAIL_FILE" "0" || true
|
||||
}
|
||||
|
||||
restart_cooldown_active() {
|
||||
local now last elapsed
|
||||
[[ -f "$LAST_RESTART_FILE" ]] || return 1
|
||||
now="$(date +%s)"
|
||||
last="$(cat "$LAST_RESTART_FILE" 2>/dev/null || echo 0)"
|
||||
elapsed=$((now - last))
|
||||
[[ "$elapsed" -lt "$RESTART_COOLDOWN_SEC" ]]
|
||||
}
|
||||
|
||||
mark_restart_attempt() {
|
||||
write_state_file "$LAST_RESTART_FILE" "$(date +%s)" || true
|
||||
}
|
||||
|
||||
restart_nexus() {
|
||||
local cname="$1" via=""
|
||||
if [[ -n "$cname" ]]; then
|
||||
docker_cmd restart "$cname" >/dev/null 2>&1 && via="Docker 容器 ${cname}"
|
||||
elif command -v supervisorctl >/dev/null 2>&1 && supervisorctl status "$NEXUS_SERVICE" >/dev/null 2>&1; then
|
||||
supervisorctl restart "$NEXUS_SERVICE" >/dev/null 2>&1 && via="Supervisor ${NEXUS_SERVICE}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
echo "$via"
|
||||
}
|
||||
|
||||
telegram_health_fail_cooldown() {
|
||||
local body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】健康检查连续失败(冷却期内)</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
连续失败:已达 ${MAX_FAIL} 次
|
||||
Docker 容器:${NEXUS_CONTAINER:-未识别}
|
||||
具体情况:/health 仍不可达,但距上次自动重启不足 ${RESTART_COOLDOWN_SEC} 秒,本次跳过重启以免频繁抖动
|
||||
建议操作:查看 docker logs ${NEXUS_CONTAINER:-nexus} 与 ${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_fail_restart() {
|
||||
local body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】Nexus 连续 ${MAX_FAIL} 次无响应</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
连续失败:${FAIL} 次(阈值 ${MAX_FAIL} 次)
|
||||
Docker 容器:${NEXUS_CONTAINER:-未识别}
|
||||
具体情况:宿主机 cron 无法访问 API 健康端点,平台可能已宕机或端口异常
|
||||
正在执行:自动重启 Nexus 服务…
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_recovered() {
|
||||
local via="$1" body
|
||||
body="$(cat <<EOF
|
||||
🟢 <b>【Layer 3 宿主机巡检】Nexus 已自动恢复</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
恢复方式:${via}
|
||||
具体情况:自动重启后 /health 检测通过,服务已恢复在线
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_still_down() {
|
||||
local via="$1" body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】自动重启后仍未恢复</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
已尝试:${via}
|
||||
具体情况:重启完成后 /health 仍失败,可能存在数据库/Redis 故障或配置错误
|
||||
建议操作:立即 SSH 登录排查 docker logs、MySQL、Redis,需人工介入
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_restart_failed() {
|
||||
local body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】无法执行自动重启</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
具体情况:未找到可重启的 Docker 容器或 Supervisor 服务(${NEXUS_SERVICE})
|
||||
建议操作:确认 nexus 容器名称与 docker ps 输出,手动执行 docker restart 或 nx update
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
NEXUS_CONTAINER="$(resolve_nexus_container)"
|
||||
load_telegram_config "$NEXUS_CONTAINER"
|
||||
init_layer3_state
|
||||
HOST_LABEL="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo unknown)"
|
||||
TS_LABEL="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || date)"
|
||||
|
||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
reset_fail_count
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FAIL="$(bump_fail_count)"
|
||||
if [[ "$FAIL" -lt "$MAX_FAIL" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if restart_cooldown_active; then
|
||||
telegram_health_fail_cooldown
|
||||
exit 0
|
||||
fi
|
||||
|
||||
telegram_health_fail_restart
|
||||
mark_restart_attempt
|
||||
|
||||
if VIA="$(restart_nexus "$NEXUS_CONTAINER")"; then
|
||||
sleep 5
|
||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
telegram_health_recovered "$VIA"
|
||||
reset_fail_count
|
||||
else
|
||||
telegram_health_still_down "$VIA"
|
||||
fi
|
||||
else
|
||||
# Python is responding normally
|
||||
echo "0" > "$FAIL_FILE"
|
||||
telegram_health_restart_failed
|
||||
fi
|
||||
|
||||
Executable
+276
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Nexus 6.0 — 重装服务器一键入口(1Panel + Docker + Nexus)
|
||||
#
|
||||
# 适用:全新 VPS / 重装系统后,root 一条命令装完整栈。
|
||||
#
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/install-1panel-docker.sh" | bash
|
||||
#
|
||||
# 4 核 16G + 自定义域名:
|
||||
# curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --profile 4c16g --domain api.example.com
|
||||
#
|
||||
# 已装 1Panel,只装 Nexus Docker:
|
||||
# curl -fsSL ".../install-1panel-docker.sh" | bash -s -- --skip-1panel
|
||||
#
|
||||
# 流程:
|
||||
# ① 可选:官方 1Panel 安装(含 Docker,交互式)
|
||||
# ② 检测 / 安装 Docker Compose
|
||||
# ③ install-nexus-fresh.sh → MySQL + Nexus 容器(Redis 自行安装)
|
||||
#
|
||||
# 反代:必须在 1Panel 面板 UI 配置,禁止脚本直接改 /opt/1panel/apps/
|
||||
# 示例: deploy/1panel/openresty-nexus.conf.example
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
|
||||
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
|
||||
PANEL_INSTALL_URL="${PANEL_INSTALL_URL:-https://resource.1panel.pro/v2/quick_start.sh}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
WITH_1PANEL=auto
|
||||
SKIP_1PANEL=false
|
||||
SKIP_DOCKER_INSTALL=false
|
||||
EXTRA_ARGS=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
重装服务器 — 1Panel + Docker + Nexus 一键安装
|
||||
|
||||
curl -fsSL "${NEXUS_RAW_BASE}/deploy/install-1panel-docker.sh" | bash
|
||||
|
||||
选项(传给 bash -s --):
|
||||
--with-1panel 未装 1Panel 时先跑官方安装脚本(交互式)
|
||||
--skip-1panel 跳过 1Panel,只装 Nexus Docker 栈
|
||||
--skip-docker-install 不自动装 Docker(已在 1Panel 装好)
|
||||
--profile 2c8g|4c16g|1c4g
|
||||
--domain NAME
|
||||
--root PATH
|
||||
-h, --help
|
||||
|
||||
说明:
|
||||
- 1Panel 官方安装为交互式(端口/用户/密码),装完记下面板地址
|
||||
- Nexus 密钥按本机自动生成,备份在 /root/.nexus-install-secrets-*.env
|
||||
- 网站反代请在 1Panel UI 操作,勿手工改 /opt/1panel/apps/
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--with-1panel) WITH_1PANEL=true; shift ;;
|
||||
--skip-1panel) SKIP_1PANEL=true; WITH_1PANEL=false; shift ;;
|
||||
--skip-docker-install) SKIP_DOCKER_INSTALL=true; shift ;;
|
||||
--profile|--domain|--root)
|
||||
EXTRA_ARGS+=("$1" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--skip-clone|--skip-1panel-check)
|
||||
EXTRA_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
*) EXTRA_ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
banner() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} Nexus — 1Panel + Docker + Nexus 一键安装 ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}║${NC} 域名: ${NEXUS_DOMAIN} 档位: ${NEXUS_PROFILE} ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行(su - 后直接运行)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
load_deploy_env() {
|
||||
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
|
||||
done
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
}
|
||||
|
||||
has_1panel() {
|
||||
command -v 1pctl >/dev/null 2>&1
|
||||
}
|
||||
|
||||
has_docker_compose() {
|
||||
docker compose version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
phase_1panel() {
|
||||
if [[ "$SKIP_1PANEL" == true ]]; then
|
||||
info "跳过 1Panel 安装(--skip-1panel)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if has_1panel; then
|
||||
info "已安装 1Panel"
|
||||
if 1pctl user-info >/dev/null 2>&1; then
|
||||
1pctl user-info 2>/dev/null | grep -E '面板地址|面板用户' || true
|
||||
else
|
||||
warn "1pctl user-info 失败,面板可能需修复;继续安装 Nexus…"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$WITH_1PANEL" == auto ]]; then
|
||||
echo ""
|
||||
warn "未检测到 1Panel(1pctl 不存在)"
|
||||
echo " ① 先装 1Panel(推荐,含 Docker)再装 Nexus"
|
||||
echo " ② 跳过 1Panel,仅装 Nexus Docker(需本机已有 Docker)"
|
||||
echo ""
|
||||
read -r -p "是否现在安装 1Panel? [Y/n]: " ans
|
||||
case "${ans:-Y}" in
|
||||
n|N) WITH_1PANEL=false ;;
|
||||
*) WITH_1PANEL=true ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ "$WITH_1PANEL" != true ]]; then
|
||||
warn "未安装 1Panel,将仅安装 Nexus Docker 栈"
|
||||
return 0
|
||||
fi
|
||||
|
||||
step "安装 1Panel(官方脚本,交互式)..."
|
||||
info "下载: $PANEL_INSTALL_URL"
|
||||
local tmp
|
||||
tmp="$(mktemp /tmp/1panel-quick-start.XXXXXX.sh)"
|
||||
if ! curl -fsSL "$PANEL_INSTALL_URL" -o "$tmp"; then
|
||||
error "下载 1Panel 安装脚本失败"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$tmp"
|
||||
echo ""
|
||||
warn "接下来为 1Panel 官方安装向导,请按提示设置端口、安全入口、用户名、密码"
|
||||
warn "安装目录建议默认 /opt"
|
||||
echo ""
|
||||
bash "$tmp"
|
||||
rm -f "$tmp"
|
||||
|
||||
if ! has_1panel; then
|
||||
error "1Panel 安装后仍未找到 1pctl,请检查安装日志"
|
||||
exit 1
|
||||
fi
|
||||
info "1Panel 安装完成"
|
||||
1pctl user-info 2>/dev/null || true
|
||||
echo ""
|
||||
warn "请在 1Panel → 应用商店 安装 OpenResty(用于 HTTPS 反代)"
|
||||
read -r -p "OpenResty 已装好或稍后自行安装,按 Enter 继续 Nexus 安装..."
|
||||
}
|
||||
|
||||
install_docker_engine() {
|
||||
step "安装 Docker Engine + Compose 插件..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ca-certificates curl >/dev/null
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable --now docker 2>/dev/null || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y -q docker docker-compose-plugin >/dev/null 2>&1 || \
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable --now docker 2>/dev/null || true
|
||||
else
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
fi
|
||||
}
|
||||
|
||||
phase_docker() {
|
||||
step "检测 Docker Compose..."
|
||||
if has_docker_compose; then
|
||||
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version | head -1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_DOCKER_INSTALL" == true ]]; then
|
||||
error "未检测到 docker compose,且已指定 --skip-docker-install"
|
||||
error "请在 1Panel → 容器 安装 Docker,或去掉该参数"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
warn "未检测到 docker compose"
|
||||
if has_1panel; then
|
||||
warn "建议在 1Panel → 容器 安装 Docker 后重新运行本脚本"
|
||||
read -r -p "是否尝试用 get.docker.com 自动安装? [y/N]: " ans
|
||||
[[ "${ans:-N}" == y || "${ans:-N}" == Y ]] || exit 1
|
||||
else
|
||||
info "自动安装 Docker(get.docker.com)..."
|
||||
fi
|
||||
|
||||
install_docker_engine
|
||||
|
||||
if ! has_docker_compose; then
|
||||
error "Docker 安装后仍无 compose 插件,请手动安装 docker-compose-plugin"
|
||||
exit 1
|
||||
fi
|
||||
info "Docker Compose 就绪"
|
||||
}
|
||||
|
||||
download_fresh_installer() {
|
||||
local name="install-nexus-fresh.sh" dest
|
||||
dest="$(mktemp /tmp/install-nexus-fresh.XXXXXX.sh)"
|
||||
local url base
|
||||
for base in "${NEXUS_RAW_BASE}" "${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/${name}"
|
||||
if curl -fsSL "$url" -o "$dest" 2>/dev/null && head -1 "$dest" | grep -q '^#!/'; then
|
||||
chmod +x "$dest"
|
||||
echo "$dest"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
phase_nexus() {
|
||||
step "安装 Nexus Docker 栈(MySQL + Nexus;Redis 请自行安装)..."
|
||||
local installer
|
||||
if [[ -x "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" ]]; then
|
||||
installer="${NEXUS_ROOT}/deploy/install-nexus-fresh.sh"
|
||||
info "使用本地: $installer"
|
||||
exec bash "$installer" --skip-1panel-check "${EXTRA_ARGS[@]}"
|
||||
fi
|
||||
|
||||
if installer="$(download_fresh_installer)"; then
|
||||
info "已下载 install-nexus-fresh.sh"
|
||||
exec bash "$installer" --skip-1panel-check "${EXTRA_ARGS[@]}"
|
||||
fi
|
||||
|
||||
error "无法获取 install-nexus-fresh.sh,请检查 Gitea 可达性"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main() {
|
||||
banner
|
||||
require_root
|
||||
load_deploy_env
|
||||
phase_1panel
|
||||
phase_docker
|
||||
phase_nexus
|
||||
}
|
||||
|
||||
# curl | bash 时 stdin 脚本无 BASH_SOURCE[0],不可配合 set -u 做入口判断
|
||||
main "$@"
|
||||
Executable
+310
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Nexus 6.0 — 全新安装脚本(1Panel + Docker Compose)
|
||||
#
|
||||
# 适用:新机已装 1Panel / Docker,空库,走 /app/install.html 安装向导
|
||||
# 风格参考 1Panel quick_start.sh:分阶段检查 + 结束输出访问地址
|
||||
#
|
||||
# 用法(公共仓库,root 下一条命令,参考 1Panel):
|
||||
#
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
#
|
||||
# 已 clone 到 /opt/nexus:
|
||||
# cd /opt/nexus && bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
#
|
||||
# bash deploy/install-nexus-fresh.sh --profile 4c16g
|
||||
#
|
||||
# 公共仓库匿名 clone,无需 Gitea 令牌。NEXUS 密钥由安装脚本按机自动生成。
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_install_self="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
if ! head -1 "$_install_self" | grep -q '^#!/'; then
|
||||
echo "[ERROR] 脚本文件无效。若用 curl 下载,请改用: bash deploy/download-install-fresh.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -qiE '^(Not found|404)' "$_install_self" 2>/dev/null; then
|
||||
echo "[ERROR] 下载到的是 Gitea 404 页面,不是安装脚本。" >&2
|
||||
echo " 请: cd /opt/nexus && git pull && sudo bash deploy/install-nexus-fresh.sh --skip-clone" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── 默认配置(与 SECRETS.md / nexus-1panel 一致)──
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
|
||||
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
|
||||
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Nexus 6.0 全新安装(1Panel + Docker)
|
||||
|
||||
sudo bash install-nexus-fresh.sh [选项]
|
||||
|
||||
选项:
|
||||
--domain NAME 公网域名(默认: ${NEXUS_DOMAIN})
|
||||
--profile NAME 资源档位 1c4g|2c8g|4c16g(默认: 2c8g)
|
||||
--root PATH 安装目录(默认: /opt/nexus)
|
||||
--skip-clone 已在 /opt/nexus,不重新 clone
|
||||
--skip-1panel-check 不检测 1Panel 是否已安装
|
||||
-h, --help 显示帮助
|
||||
|
||||
安装后请用浏览器打开: https://域名/app/install.html
|
||||
root 下全局命令: nexus-fresh | nexus-update | nx
|
||||
EOF
|
||||
}
|
||||
|
||||
SKIP_1PANEL_CHECK=false
|
||||
SKIP_CLONE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--domain) NEXUS_DOMAIN="$2"; shift 2 ;;
|
||||
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
||||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||
--skip-clone) SKIP_CLONE=true; shift ;;
|
||||
--skip-1panel-check) SKIP_1PANEL_CHECK=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
banner() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} Nexus 6.0 — 全新安装(1Panel + Docker Compose) ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}║${NC} 域名: ${NEXUS_DOMAIN} ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}║${NC} 档位: ${NEXUS_PROFILE} 目录: ${NEXUS_ROOT} ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
load_credentials() {
|
||||
step "加载凭据..."
|
||||
local f
|
||||
for f in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
if [[ -f "$f" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "$f"
|
||||
set +a
|
||||
info "已加载 $f"
|
||||
fi
|
||||
done
|
||||
if [[ -f "${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh"
|
||||
set +a
|
||||
info "已加载 ${NEXUS_ROOT}/deploy/nexus-1panel.secrets.sh(可选)"
|
||||
fi
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
info "全新安装:NEXUS 密钥将在 docker/.env.prod 中按本机自动生成"
|
||||
}
|
||||
|
||||
install_root_commands() {
|
||||
[[ -d "${NEXUS_ROOT}/deploy" ]] || return 0
|
||||
NEXUS_ROOT="$NEXUS_ROOT" bash "${NEXUS_ROOT}/deploy/install-nx-cli.sh" || true
|
||||
info "全局命令: nx · nexus-update · nexus-fresh · nexus-install · nexus-1panel"
|
||||
}
|
||||
|
||||
phase_preflight() {
|
||||
step "阶段 1/6 — 环境检查"
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请切换到 root 再执行(不要用 sudo 包一层):"
|
||||
error " su -"
|
||||
error " bash $0 ..."
|
||||
exit 1
|
||||
fi
|
||||
info "运行用户: root(无需 sudo)"
|
||||
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
error "无法识别操作系统"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
info "系统: ${NAME:-Linux} ${VERSION_ID:-}"
|
||||
case "${ID:-}" in
|
||||
ubuntu|debian) info "受支持的分支: Debian/Ubuntu ✓" ;;
|
||||
centos|rhel|rocky|almalinux) info "受支持的分支: RHEL 系 ✓" ;;
|
||||
*) warn "未在官方列表中测试,可能仍可运行" ;;
|
||||
esac
|
||||
|
||||
local mem_kb
|
||||
mem_kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||
if [[ "$mem_kb" -lt 3500000 ]]; then
|
||||
warn "内存 < 4GB,建议档位 1c4g: --profile 1c4g"
|
||||
else
|
||||
info "内存: $((mem_kb / 1024 / 1024)) GiB 量级"
|
||||
fi
|
||||
}
|
||||
|
||||
phase_1panel() {
|
||||
step "阶段 2/6 — 1Panel / Docker"
|
||||
if [[ "$SKIP_1PANEL_CHECK" != true ]]; then
|
||||
if command -v 1pctl >/dev/null 2>&1; then
|
||||
info "检测到 1Panel (1pctl)"
|
||||
if 1pctl version >/dev/null 2>&1; then
|
||||
info "1Panel: $(1pctl version 2>/dev/null | head -1 || true)"
|
||||
fi
|
||||
else
|
||||
warn "未检测到 1pctl — 若尚未安装 1Panel,可先执行:"
|
||||
warn " curl -sSL https://resource.1panel.pro/v2/quick_start.sh -o quick_start.sh"
|
||||
warn " bash quick_start.sh"
|
||||
read -r -p "继续安装 Nexus? [y/N]: " cont
|
||||
[[ "$cont" == "y" || "$cont" == "Y" ]] || exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
error "未检测到 docker compose"
|
||||
error "请在 1Panel → 容器 中安装 Docker,或: apt install -y docker.io docker-compose-v2"
|
||||
exit 1
|
||||
fi
|
||||
info "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version | head -1)"
|
||||
}
|
||||
|
||||
phase_packages() {
|
||||
step "阶段 3/6 — 依赖包"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git curl openssl ca-certificates iproute2 >/dev/null 2>&1 || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y -q git curl openssl ca-certificates iproute >/dev/null 2>&1 || true
|
||||
fi
|
||||
info "git / curl 就绪"
|
||||
}
|
||||
|
||||
git_clone_url() {
|
||||
if [[ -z "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
echo "${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
else
|
||||
echo "${NEXUS_GITEA_SCHEME}://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
}
|
||||
|
||||
phase_clone() {
|
||||
step "阶段 4/6 — 获取 Nexus 代码"
|
||||
if [[ "$SKIP_CLONE" == true ]]; then
|
||||
if [[ ! -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
error "--skip-clone 但 ${NEXUS_ROOT} 不是 git 仓库"
|
||||
exit 1
|
||||
fi
|
||||
load_credentials
|
||||
info "跳过 clone,使用现有 ${NEXUS_ROOT}"
|
||||
info "版本: $(git -C "$NEXUS_ROOT" log -1 --oneline 2>/dev/null || echo '?')"
|
||||
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
load_credentials
|
||||
local url
|
||||
url="$(git_clone_url)"
|
||||
if [[ -z "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
info "公共仓库:匿名 git clone(无需令牌)"
|
||||
fi
|
||||
mkdir -p "$(dirname "$NEXUS_ROOT")"
|
||||
|
||||
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
info "仓库已存在,同步 ${NEXUS_GIT_BRANCH}..."
|
||||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||||
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --prune
|
||||
git -C "$NEXUS_ROOT" reset --hard "origin/${NEXUS_GIT_BRANCH}"
|
||||
else
|
||||
if [[ -d "$NEXUS_ROOT" ]] && [[ -n "$(ls -A "$NEXUS_ROOT" 2>/dev/null)" ]]; then
|
||||
error "目录非空且不是 git 仓库: $NEXUS_ROOT"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$NEXUS_ROOT"
|
||||
info "克隆 → $NEXUS_ROOT"
|
||||
git clone "$url" "$NEXUS_ROOT"
|
||||
git -C "$NEXUS_ROOT" checkout "$NEXUS_GIT_BRANCH" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
load_credentials
|
||||
info "版本: $(git -C "$NEXUS_ROOT" log -1 --oneline)"
|
||||
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
|
||||
}
|
||||
|
||||
phase_install_stack() {
|
||||
step "阶段 5/6 — Docker 构建与启动(全新库,约 10~20 分钟)"
|
||||
export NEXUS_ROOT NEXUS_DOMAIN NEXUS_PROFILE
|
||||
bash "${NEXUS_ROOT}/deploy/nexus-1panel.sh" install \
|
||||
--fresh \
|
||||
--skip-clone \
|
||||
--profile "$NEXUS_PROFILE" \
|
||||
--domain "$NEXUS_DOMAIN"
|
||||
}
|
||||
|
||||
phase_post() {
|
||||
step "阶段 6/6 — 收尾"
|
||||
install_root_commands
|
||||
|
||||
local port health
|
||||
port="8600"
|
||||
health="$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || echo "pending")"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}Nexus 全新安装完成${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╠══════════════════════════════════════════════════════════════╣${NC}"
|
||||
echo -e "${GREEN}║${NC} 本地健康: http://127.0.0.1:${port}/health → ${health} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}① 浏览器完成安装向导(必做)${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} https://${NEXUS_DOMAIN}/app/install.html ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}② 1Panel 创建网站并反代${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} 目标: http://127.0.0.1:${port} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} 示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} ${YELLOW}③ 日常运维${NC} ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nx # 统一菜单(一键更新 / 重启 / 日志) ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}║${NC} nexus-update # 等同 deploy/update.sh ${GREEN}║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
warn_if_already_installed() {
|
||||
local env_prod="${NEXUS_ROOT}/docker/.env.prod" cname
|
||||
[[ -f "$env_prod" ]] || return 0
|
||||
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -n "$cname" ]] && docker exec "$cname" test -f /var/lib/nexus/.install_locked 2>/dev/null; then
|
||||
warn "检测到已安装并锁定的 Nexus(将清除 nexus-state 卷内配置并重建)"
|
||||
read -r -p "确认继续全新安装? [y/N]: " ok
|
||||
[[ "$ok" == "y" || "$ok" == "Y" ]] || exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
banner
|
||||
if [[ "$SKIP_CLONE" == true && -d "${NEXUS_ROOT}/deploy" ]]; then
|
||||
install_root_commands
|
||||
fi
|
||||
warn_if_already_installed
|
||||
phase_preflight
|
||||
phase_1panel
|
||||
phase_packages
|
||||
phase_clone
|
||||
phase_install_stack
|
||||
phase_post
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# 注册 Nexus 全局 CLI(/usr/local/bin)— install / upgrade / nx 均会调用
|
||||
#
|
||||
# NEXUS_ROOT=/opt/nexus bash deploy/install-nx-cli.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
BIN_DIR="${NEXUS_BIN_DIR:-/usr/local/bin}"
|
||||
DEPLOY="${NEXUS_ROOT}/deploy"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
if [[ ! -d "$DEPLOY" ]]; then
|
||||
error "未找到 $DEPLOY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
warn "非 root:无法写入 $BIN_DIR,请使用 root 执行或: sudo bash $DEPLOY/install-nx-cli.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
chmod +x \
|
||||
"$DEPLOY/nx" \
|
||||
"$DEPLOY/update.sh" \
|
||||
"$DEPLOY/nexus-1panel.sh" \
|
||||
"$DEPLOY/install-nexus-fresh.sh" \
|
||||
"$DEPLOY/quick-install.sh" \
|
||||
"$DEPLOY/install-1panel-docker.sh" \
|
||||
"$DEPLOY/upgrade-1panel-docker.sh" \
|
||||
"$DEPLOY/sync-install-wizard-to-container.sh" \
|
||||
"$DEPLOY/uninstall-mysql-compose.sh" \
|
||||
"$DEPLOY/uninstall-redis-compose.sh" \
|
||||
"$DEPLOY/install-nx-cli.sh" \
|
||||
2>/dev/null || true
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
link_cli() {
|
||||
local src="$1" name="$2"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
warn "跳过 $name(缺少 $src)"
|
||||
return 0
|
||||
fi
|
||||
ln -sf "$src" "${BIN_DIR}/${name}"
|
||||
}
|
||||
|
||||
link_cli "$DEPLOY/nx" nx
|
||||
link_cli "$DEPLOY/update.sh" nexus-update
|
||||
link_cli "$DEPLOY/install-nexus-fresh.sh" nexus-fresh
|
||||
link_cli "$DEPLOY/quick-install.sh" nexus-install
|
||||
link_cli "$DEPLOY/nexus-1panel.sh" nexus-1panel
|
||||
link_cli "$DEPLOY/install-1panel-docker.sh" nexus-1panel-docker
|
||||
|
||||
if command -v nx >/dev/null 2>&1; then
|
||||
info "全局命令已就绪: nx → $(readlink -f "$(command -v nx)" 2>/dev/null || command -v nx)"
|
||||
else
|
||||
error "nx 未出现在 PATH 中,请确认 $BIN_DIR 在 PATH 内"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,526 @@
|
||||
#!/bin/bash
|
||||
# ================================================================
|
||||
# Nexus 6.0 — One-Click Install Script
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://YOUR_DOMAIN/deploy/install.sh | sudo bash -s -- --domain nexus.example.com
|
||||
# sudo bash install.sh --domain nexus.example.com
|
||||
#
|
||||
# Supports: Ubuntu 20.04+, Debian 11+, CentOS 7/8, Rocky 8/9, AlmaLinux 8/9
|
||||
# Auto-detects BT Panel (宝塔面板) and adapts paths accordingly.
|
||||
# ================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors ──
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
# ── Helper: Idempotent config write ──
|
||||
write_config() {
|
||||
local target="$1" content="$2"
|
||||
if [ -f "$target" ]; then
|
||||
local existing
|
||||
existing=$(cat "$target" 2>/dev/null || true)
|
||||
if [ "$existing" = "$content" ]; then
|
||||
info " [skip] $target (unchanged)"
|
||||
return 0
|
||||
fi
|
||||
info " [update] $target (content changed)"
|
||||
else
|
||||
info " [create] $target"
|
||||
fi
|
||||
echo "$content" > "$target"
|
||||
}
|
||||
|
||||
# ── Defaults ──
|
||||
DOMAIN=""
|
||||
DEPLOY_DIR=""
|
||||
PORT=8600
|
||||
SKIP_NGINX=false
|
||||
SKIP_SUPERVISOR=false
|
||||
BT_MODE=false
|
||||
NEXUS_REPO=""
|
||||
|
||||
# ── Parse Arguments ──
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--domain) DOMAIN="$2"; shift 2 ;;
|
||||
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
|
||||
--port) PORT="$2"; shift 2 ;;
|
||||
--skip-nginx) SKIP_NGINX=true; shift ;;
|
||||
--skip-supervisor) SKIP_SUPERVISOR=true; shift ;;
|
||||
--repo) NEXUS_REPO="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: sudo bash install.sh --domain DOMAIN [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --domain DOMAIN Required. Domain or IP for Nginx server_name"
|
||||
echo " --deploy-dir PATH Installation directory (auto-detected)"
|
||||
echo " --port PORT uvicorn port (default: 8600)"
|
||||
echo " --skip-nginx Skip Nginx configuration"
|
||||
echo " --skip-supervisor Skip Supervisor configuration"
|
||||
echo " --repo URL Git repository URL (optional, for cloning)"
|
||||
exit 0 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Banner ──
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Nexus 6.0 Install Script"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# ================================================================
|
||||
# Phase 1: Pre-flight Checks
|
||||
# ================================================================
|
||||
|
||||
info "Phase 1/7: Pre-flight checks..."
|
||||
|
||||
# 1.1 Must run as root
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
error "This script must be run as root. Use: sudo bash install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1.2 Domain is required
|
||||
if [ -z "$DOMAIN" ]; then
|
||||
error "--domain is required. Usage: sudo bash install.sh --domain nexus.example.com"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1.3 OS detection
|
||||
if [ ! -f /etc/os-release ]; then
|
||||
error "Cannot detect OS. /etc/os-release not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OS_ID=$(grep '^ID=' /etc/os-release | head -1 | cut -d'=' -f2 | tr -d '"' | cut -d'-' -f1)
|
||||
OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release | head -1 | cut -d'=' -f2 | tr -d '"')
|
||||
|
||||
case "$OS_ID" in
|
||||
ubuntu|debian) PKG_MANAGER="apt" ;;
|
||||
centos|rocky|almalinux|rhel) PKG_MANAGER="yum" ;;
|
||||
*)
|
||||
warn "Unsupported OS: $OS_ID. Script may still work but is untested."
|
||||
PKG_MANAGER="apt"
|
||||
;;
|
||||
esac
|
||||
info " OS: $OS_ID $OS_VERSION (pkg: $PKG_MANAGER)"
|
||||
|
||||
# 1.4 BT Panel detection
|
||||
if [ -d "/www/server/panel" ] && [ -f "/www/server/panel/class/common.py" ]; then
|
||||
BT_MODE=true
|
||||
info " BT Panel: detected"
|
||||
else
|
||||
info " BT Panel: not detected (standard mode)"
|
||||
fi
|
||||
|
||||
# 1.5 Set deploy directory
|
||||
if [ -z "$DEPLOY_DIR" ]; then
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
DEPLOY_DIR="/www/wwwroot/$DOMAIN"
|
||||
else
|
||||
DEPLOY_DIR="/opt/nexus"
|
||||
fi
|
||||
fi
|
||||
VENV_DIR="$DEPLOY_DIR/venv"
|
||||
|
||||
# Set log paths early (used in multiple phases)
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
LOG_DIR="/www/wwwlogs"
|
||||
else
|
||||
LOG_DIR="/var/log/nexus"
|
||||
fi
|
||||
|
||||
info " Deploy dir: $DEPLOY_DIR"
|
||||
info " Domain: $DOMAIN"
|
||||
info " Port: $PORT"
|
||||
|
||||
# ================================================================
|
||||
# Phase 2: System Dependencies
|
||||
# ================================================================
|
||||
|
||||
info "Phase 2/7: Installing system dependencies..."
|
||||
|
||||
if [ "$PKG_MANAGER" = "apt" ]; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq \
|
||||
python3 python3-venv python3-pip python3-dev \
|
||||
build-essential libssl-dev libffi-dev default-libmysqlclient-dev \
|
||||
git curl >/dev/null 2>&1 || warn "Some apt packages failed to install (may already exist)"
|
||||
elif [ "$PKG_MANAGER" = "yum" ]; then
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y -q \
|
||||
python3 python3-devel python3-pip \
|
||||
gcc openssl-devel libffi-devel mysql-devel \
|
||||
git curl >/dev/null 2>&1 || warn "Some dnf packages failed to install"
|
||||
else
|
||||
yum install -y -q \
|
||||
python3 python3-devel python3-pip \
|
||||
gcc openssl-devel libffi-devel mysql-devel \
|
||||
git curl >/dev/null 2>&1 || warn "Some yum packages failed to install"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ================================================================
|
||||
# Phase 3: Python 3.10+ Resolution
|
||||
# ================================================================
|
||||
|
||||
info "Phase 3/7: Resolving Python 3.10+..."
|
||||
|
||||
PYTHON=""
|
||||
CANDIDATES="python3.12 python3.11 python3.10 /usr/bin/python3.12 /usr/bin/python3.11 /usr/bin/python3.10 python3"
|
||||
|
||||
# Add BT Panel Python paths
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
for p in /www/server/python-manager/versions/*/bin/python3.*; do
|
||||
[ -x "$p" ] && CANDIDATES="$CANDIDATES $p"
|
||||
done
|
||||
fi
|
||||
|
||||
for candidate in $CANDIDATES; do
|
||||
if command -v "$candidate" >/dev/null 2>&1 || [ -x "$candidate" ]; then
|
||||
ver=$("$candidate" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)
|
||||
if [ -n "$ver" ]; then
|
||||
major=$(echo "$ver" | cut -d'.' -f1)
|
||||
minor=$(echo "$ver" | cut -d'.' -f2)
|
||||
if [ "$major" -eq 3 ] && [ "${minor:-0}" -ge 10 ] 2>/dev/null; then
|
||||
PYTHON="$candidate"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Fallback: install Python 3.12 on Ubuntu via deadsnakes PPA
|
||||
if [ -z "$PYTHON" ] && [ "$OS_ID" = "ubuntu" ]; then
|
||||
warn " Python 3.10+ not found. Adding deadsnakes PPA..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get install -y -qq software-properties-common >/dev/null 2>&1
|
||||
add-apt-repository -y ppa:deadsnakes/ppa >/dev/null 2>&1
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq python3.12 python3.12-venv python3.12-dev >/dev/null 2>&1
|
||||
if command -v python3.12 >/dev/null 2>&1; then
|
||||
PYTHON="python3.12"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$PYTHON" ]; then
|
||||
error "Python 3.10+ is required but not found."
|
||||
error "Install Python 3.10+ manually and re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PYTHON_VERSION=$("$PYTHON" --version 2>&1)
|
||||
info " Using: $PYTHON ($PYTHON_VERSION)"
|
||||
|
||||
# ================================================================
|
||||
# Phase 4: Application Deployment
|
||||
# ================================================================
|
||||
|
||||
info "Phase 4/7: Deploying application..."
|
||||
|
||||
# 4.1 Create deploy directory
|
||||
mkdir -p "$DEPLOY_DIR"
|
||||
|
||||
# 4.2 Download / Clone Nexus
|
||||
if [ -d "$DEPLOY_DIR/server" ] && [ -d "$DEPLOY_DIR/web" ]; then
|
||||
info " Application files already exist in $DEPLOY_DIR (skipping download)"
|
||||
if [ -d "$DEPLOY_DIR/.git" ]; then
|
||||
info " Git repo detected, pulling latest..."
|
||||
(cd "$DEPLOY_DIR" && git pull --ff-only 2>/dev/null || warn " git pull failed (may have local changes)")
|
||||
fi
|
||||
elif [ -n "$NEXUS_REPO" ]; then
|
||||
info " Cloning from $NEXUS_REPO..."
|
||||
git clone "$NEXUS_REPO" "$DEPLOY_DIR" 2>/dev/null || {
|
||||
error "git clone failed. Check the repository URL."
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
error "No application files found in $DEPLOY_DIR and no --repo URL provided."
|
||||
error "Either:"
|
||||
error " 1. Copy Nexus files to $DEPLOY_DIR manually and re-run"
|
||||
error " 2. Use --repo https://your-gitea-url/Nexus.git"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4.3 Create Python venv
|
||||
if [ -d "$VENV_DIR/bin" ] && "$VENV_DIR/bin/python" -c 'import sys; v=sys.version_info; assert v.major==3 and v.minor>=10' 2>/dev/null; then
|
||||
info " venv already exists with Python 3.10+ (skipping)"
|
||||
else
|
||||
info " Creating Python venv..."
|
||||
rm -rf "$VENV_DIR"
|
||||
"$PYTHON" -m venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# 4.4 Install pip dependencies
|
||||
info " Installing Python dependencies (this may take a few minutes)..."
|
||||
"$VENV_DIR/bin/pip" install -q --upgrade pip
|
||||
"$VENV_DIR/bin/pip" install -q -r "$DEPLOY_DIR/requirements.txt" 2>/dev/null || {
|
||||
warn " pip install had some warnings, but continuing..."
|
||||
}
|
||||
|
||||
# 4.5 Create log directory
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# 4.6 Create backup directory
|
||||
mkdir -p "$DEPLOY_DIR/backups"
|
||||
|
||||
# ================================================================
|
||||
# Phase 5: Supervisor Configuration
|
||||
# ================================================================
|
||||
|
||||
if [ "$SKIP_SUPERVISOR" = true ]; then
|
||||
warn "Phase 5/7: Skipping Supervisor (per --skip-supervisor)"
|
||||
else
|
||||
info "Phase 5/7: Configuring Supervisor..."
|
||||
|
||||
# Calculate worker count
|
||||
WORKERS=$(nproc 2>/dev/null || echo 2)
|
||||
[ "$WORKERS" -gt 4 ] && WORKERS=4
|
||||
[ "$WORKERS" -lt 1 ] && WORKERS=1
|
||||
|
||||
# Set Supervisor config path
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
SUPERVISOR_CONF_DIR="/www/server/panel/plugin/supervisor/profile"
|
||||
CONF_FILE="$SUPERVISOR_CONF_DIR/nexus.ini"
|
||||
else
|
||||
SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d"
|
||||
CONF_FILE="$SUPERVISOR_CONF_DIR/nexus.conf"
|
||||
fi
|
||||
|
||||
# Generate Supervisor config
|
||||
SUPERVISOR_CONF="[program:nexus]
|
||||
command=$VENV_DIR/bin/uvicorn server.main:app --host 0.0.0.0 --port $PORT --workers $WORKERS --loop uvloop --http httptools --log-level info
|
||||
directory=$DEPLOY_DIR
|
||||
user=root
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=10
|
||||
startsecs=3
|
||||
stopwaitsecs=10
|
||||
stopsignal=INT
|
||||
environment=PATH=\"$VENV_DIR/bin:%(ENV_PATH)s\",HOME=\"/root\",NEXUS_WORKERS=\"$WORKERS\"
|
||||
stderr_logfile=$LOG_DIR/nexus_error.log
|
||||
stdout_logfile=$LOG_DIR/nexus_access.log
|
||||
stderr_logfile_maxbytes=50MB
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stderr_logfile_backups=5
|
||||
stdout_logfile_backups=5"
|
||||
|
||||
# Write config (idempotent)
|
||||
mkdir -p "$SUPERVISOR_CONF_DIR"
|
||||
write_config "$CONF_FILE" "$SUPERVISOR_CONF"
|
||||
|
||||
# Ensure Supervisor is running
|
||||
if ! supervisorctl status >/dev/null 2>&1; then
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
if [ -f /etc/init.d/supervisor ]; then
|
||||
/etc/init.d/supervisor start >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
systemctl start supervisor >/dev/null 2>&1 || true
|
||||
fi
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Reload Supervisor config
|
||||
supervisorctl reread >/dev/null 2>&1 || true
|
||||
supervisorctl update >/dev/null 2>&1 || true
|
||||
info " Supervisor config written to $CONF_FILE (workers=$WORKERS)"
|
||||
fi
|
||||
|
||||
# ================================================================
|
||||
# Phase 6: Nginx Configuration
|
||||
# ================================================================
|
||||
|
||||
if [ "$SKIP_NGINX" = true ]; then
|
||||
warn "Phase 6/7: Skipping Nginx (per --skip-nginx)"
|
||||
else
|
||||
info "Phase 6/7: Configuring Nginx..."
|
||||
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
NGINX_CONF_FILE="/www/server/panel/vhost/nginx/$DOMAIN.conf"
|
||||
NGINX_RELOAD_CMD="/www/server/nginx/sbin/nginx -s reload"
|
||||
NGINX_TEST_CMD="/www/server/nginx/sbin/nginx -t"
|
||||
NGINX_ACCESS_LOG="/www/wwwlogs/$DOMAIN.log"
|
||||
NGINX_ERROR_LOG="/www/wwwlogs/$DOMAIN.error.log"
|
||||
else
|
||||
NGINX_CONF_FILE="/etc/nginx/sites-available/nexus"
|
||||
NGINX_RELOAD_CMD="systemctl reload nginx"
|
||||
NGINX_TEST_CMD="nginx -t"
|
||||
NGINX_ACCESS_LOG="/var/log/nginx/nexus_access.log"
|
||||
NGINX_ERROR_LOG="/var/log/nginx/nexus_error.log"
|
||||
fi
|
||||
|
||||
NGINX_CONF="# Nexus 6.0 — Nginx Reverse Proxy
|
||||
# Auto-generated by install.sh
|
||||
upstream nexus_api {
|
||||
server 127.0.0.1:$PORT;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name $DOMAIN;
|
||||
|
||||
# Reverse proxy to Nexus (uvicorn)
|
||||
location / {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
|
||||
# WebSocket support (terminal, alerts)
|
||||
location /ws/ {
|
||||
proxy_pass http://nexus_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection \"upgrade\";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
|
||||
# Static assets caching
|
||||
location /app/vendor/ {
|
||||
proxy_pass http://nexus_api;
|
||||
expires 7d;
|
||||
add_header Cache-Control \"public, immutable\";
|
||||
}
|
||||
|
||||
# Block sensitive files
|
||||
location ~ ^/(\\.env|\\.git|\\.svn) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Block config.json access
|
||||
location ~ /data/config\\.json$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Upload limit
|
||||
client_max_body_size 500m;
|
||||
|
||||
# Well-known for SSL cert validation
|
||||
location ~ \\.well-known {
|
||||
allow all;
|
||||
}
|
||||
|
||||
access_log $NGINX_ACCESS_LOG;
|
||||
error_log $NGINX_ERROR_LOG;
|
||||
}"
|
||||
|
||||
# Write Nginx config
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
write_config "$NGINX_CONF_FILE" "$NGINX_CONF"
|
||||
else
|
||||
mkdir -p /etc/nginx/sites-available
|
||||
write_config "$NGINX_CONF_FILE" "$NGINX_CONF"
|
||||
if [ ! -L /etc/nginx/sites-enabled/nexus ]; then
|
||||
ln -sf "$NGINX_CONF_FILE" /etc/nginx/sites-enabled/nexus
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test and reload Nginx
|
||||
if $NGINX_TEST_CMD >/dev/null 2>&1; then
|
||||
$NGINX_RELOAD_CMD >/dev/null 2>&1 || warn " Nginx reload failed"
|
||||
info " Nginx config written to $NGINX_CONF_FILE"
|
||||
else
|
||||
warn " Nginx config test failed. Please check $NGINX_CONF_FILE manually"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ================================================================
|
||||
# Phase 7: Start Application + Verify
|
||||
# ================================================================
|
||||
|
||||
info "Phase 7/7: Starting application..."
|
||||
|
||||
# Start via Supervisor
|
||||
if [ "$SKIP_SUPERVISOR" = false ]; then
|
||||
supervisorctl start nexus >/dev/null 2>&1 || supervisorctl restart nexus >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Wait for health check
|
||||
HEALTH_OK=false
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
||||
HEALTH_OK=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# ── Print Results ──
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
|
||||
if [ "$HEALTH_OK" = true ]; then
|
||||
echo -e " ${GREEN}Nexus installed successfully!${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}Nexus process started but health check pending.${NC}"
|
||||
echo -e " ${YELLOW}It may need a few more seconds to start up.${NC}"
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo " Deploy: $DEPLOY_DIR"
|
||||
echo " Domain: $DOMAIN"
|
||||
echo " Port: $PORT"
|
||||
echo " Workers: ${WORKERS:-N/A}"
|
||||
echo " BT Panel: $BT_MODE"
|
||||
echo ""
|
||||
|
||||
if [ "$HEALTH_OK" = true ]; then
|
||||
echo -e " ${GREEN}Next step:${NC} Open your browser and complete the web install wizard:"
|
||||
echo ""
|
||||
echo " http://$DOMAIN/app/install.html"
|
||||
echo ""
|
||||
echo " The wizard will guide you through:"
|
||||
echo " Step 1: Welcome & prerequisites"
|
||||
echo " Step 2: Environment check"
|
||||
echo " Step 3: MySQL + Redis configuration"
|
||||
echo " Step 4: Admin account + brand"
|
||||
echo " Step 5: Complete"
|
||||
else
|
||||
echo " Troubleshooting:"
|
||||
echo " supervisorctl status nexus"
|
||||
echo " tail -f $LOG_DIR/nexus_error.log"
|
||||
echo " curl http://127.0.0.1:$PORT/health"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Post-install:"
|
||||
echo " 1. Configure SSL (HTTPS)"
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
echo " BT Panel -> Website -> $DOMAIN -> SSL"
|
||||
else
|
||||
echo " certbot --nginx -d $DOMAIN"
|
||||
fi
|
||||
echo " 2. Set up health monitor crontab:"
|
||||
echo " * * * * * $DEPLOY_DIR/deploy/health_monitor.sh"
|
||||
echo " 3. Set up database backup crontab:"
|
||||
echo " 0 3 * * * $DEPLOY_DIR/deploy/db_backup.sh"
|
||||
echo ""
|
||||
echo " Management:"
|
||||
echo " supervisorctl status nexus"
|
||||
echo " supervisorctl restart nexus"
|
||||
echo " supervisorctl stop nexus"
|
||||
echo ""
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — 安装/更新 host crontab:health_monitor + db_backup
|
||||
#
|
||||
# sudo bash deploy/install_ops_cron.sh
|
||||
# NEXUS_ROOT=/opt/nexus sudo bash deploy/install_ops_cron.sh
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
DEPLOY_DIR="${NEXUS_ROOT}/deploy"
|
||||
HEALTH_MARKER="nexus-health-monitor"
|
||||
BACKUP_MARKER="nexus-mysql-backup"
|
||||
HEALTH_LOG="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||
BACKUP_LOG="${NEXUS_BACKUP_LOG:-/var/log/nexus_backup.log}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 运行: sudo bash $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_crontab() {
|
||||
if command -v crontab >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
warn "未检测到 crontab,尝试安装 cron 包…"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq cron
|
||||
systemctl enable --now cron 2>/dev/null || systemctl enable --now crond 2>/dev/null || true
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y cronie
|
||||
systemctl enable --now crond 2>/dev/null || true
|
||||
fi
|
||||
if ! command -v crontab >/dev/null 2>&1; then
|
||||
error "无法安装 crontab(请手动: apt install cron)"
|
||||
exit 1
|
||||
fi
|
||||
info "cron 已安装"
|
||||
}
|
||||
|
||||
ensure_crontab
|
||||
|
||||
for f in \
|
||||
"${DEPLOY_DIR}/health_monitor.sh" \
|
||||
"${DEPLOY_DIR}/db_backup.sh" \
|
||||
"${DEPLOY_DIR}/mysql_dump_to_file.sh" \
|
||||
; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
error "缺少脚本: $f"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$f" 2>/dev/null || true
|
||||
done
|
||||
|
||||
touch "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
|
||||
chmod 644 "$HEALTH_LOG" "$BACKUP_LOG" 2>/dev/null || true
|
||||
|
||||
HEALTH_LINE="* * * * * NEXUS_DEPLOY_DIR=${NEXUS_ROOT} bash ${DEPLOY_DIR}/health_monitor.sh >> ${HEALTH_LOG} 2>&1 # ${HEALTH_MARKER}"
|
||||
BACKUP_LINE="0 3 * * * NEXUS_ROOT=${NEXUS_ROOT} bash ${DEPLOY_DIR}/db_backup.sh >> ${BACKUP_LOG} 2>&1 # ${BACKUP_MARKER}"
|
||||
|
||||
existing="$(crontab -l 2>/dev/null || true)"
|
||||
filtered="$(printf '%s\n' "$existing" | grep -v "$HEALTH_MARKER" | grep -v "$BACKUP_MARKER" | grep -v 'deploy/health_monitor.sh' | grep -v 'deploy/db_backup.sh' || true)"
|
||||
{
|
||||
printf '%s\n' "$filtered" | sed '/^[[:space:]]*$/d'
|
||||
echo "$HEALTH_LINE"
|
||||
echo "$BACKUP_LINE"
|
||||
} | crontab -
|
||||
|
||||
info "已写入 crontab:"
|
||||
crontab -l | grep -E "$HEALTH_MARKER|$BACKUP_MARKER" || true
|
||||
info "健康巡检日志: $HEALTH_LOG"
|
||||
info "MySQL 备份日志: $BACKUP_LOG"
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus — MySQL dump(Docker 1Panel / 宿主机 mysqldump 通用)
|
||||
#
|
||||
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql [--root /opt/nexus]
|
||||
# bash deploy/mysql_dump_to_file.sh --output /path/dump.sql --db-url 'mysql+aiomysql://...'
|
||||
#
|
||||
# 策略(按序):
|
||||
# 1. DB host 为运行中容器名 → docker exec 该容器内 mysqldump(连 127.0.0.1)
|
||||
# 2. 宿主机有 mysqldump 且 host 可解析 → 本机 mysqldump
|
||||
# 3. 1panel-network 存在 → docker run --rm mysql:8 客户端
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
OUTPUT=""
|
||||
DB_URL=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: mysql_dump_to_file.sh --output FILE.sql [--root /opt/nexus] [--db-url URL]
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output|-o) OUTPUT="$2"; shift 2 ;;
|
||||
--root) NEXUS_ROOT="$2"; shift 2 ;;
|
||||
--db-url) DB_URL="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "未知参数: $1" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
echo "缺少 --output" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
resolve_db_url() {
|
||||
if [[ -n "$DB_URL" ]]; then
|
||||
echo "$DB_URL"
|
||||
return 0
|
||||
fi
|
||||
local cname url
|
||||
cname="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -n "$cname" ]]; then
|
||||
url="$(docker exec "$cname" grep -E '^NEXUS_DATABASE_URL=' /app/.env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
||||
[[ -n "$url" ]] && echo "$url" && return 0
|
||||
fi
|
||||
for f in \
|
||||
"${NEXUS_ROOT}/docker/.env.prod" \
|
||||
"${NEXUS_ROOT}/.env" \
|
||||
; do
|
||||
[[ -f "$f" ]] || continue
|
||||
url="$(grep -E '^NEXUS_DATABASE_URL=' "$f" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'" || true)"
|
||||
[[ -n "$url" ]] && echo "$url" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
DB_URL="$(resolve_db_url || true)"
|
||||
if [[ -z "$DB_URL" ]]; then
|
||||
echo "未找到 NEXUS_DATABASE_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$DB_URL" "$OUTPUT" <<'PY'
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
|
||||
def docker_names() -> set[str]:
|
||||
r = subprocess.run(
|
||||
["docker", "ps", "--format", "{{.Names}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return {ln.strip() for ln in (r.stdout or "").splitlines() if ln.strip()}
|
||||
|
||||
|
||||
def network_exists(name: str) -> bool:
|
||||
return (
|
||||
subprocess.run(
|
||||
["docker", "network", "inspect", name],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
).returncode
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
def write_dump(cmd: list[str], env: dict[str, str], out: str) -> int:
|
||||
r = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
sys.stderr.write(r.stderr or r.stdout or "mysqldump failed\n")
|
||||
return r.returncode
|
||||
with open(out, "w", encoding="utf-8") as fh:
|
||||
fh.write(r.stdout)
|
||||
return 0
|
||||
|
||||
|
||||
def dump_flags(db: str) -> list[str]:
|
||||
return [
|
||||
"--single-transaction",
|
||||
"--routines",
|
||||
"--triggers",
|
||||
"--set-gtid-purged=OFF",
|
||||
db,
|
||||
]
|
||||
|
||||
|
||||
url = sys.argv[1].replace("mysql+aiomysql://", "mysql://", 1)
|
||||
url = url.replace("mysql+asyncmy://", "mysql://", 1)
|
||||
out = sys.argv[2]
|
||||
p = urllib.parse.urlparse(url)
|
||||
user = urllib.parse.unquote(p.username or "")
|
||||
password = urllib.parse.unquote(p.password or "")
|
||||
host = p.hostname or "127.0.0.1"
|
||||
if host == "host.docker.internal":
|
||||
host = "127.0.0.1"
|
||||
port = str(p.port or 3306)
|
||||
db = (p.path or "/nexus").lstrip("/").split("?")[0]
|
||||
env = {**__import__("os").environ, "MYSQL_PWD": password}
|
||||
names = docker_names()
|
||||
|
||||
# 1) MySQL 跑在 Docker 容器内(1Panel 常见:URL host = 容器名)
|
||||
if host in names:
|
||||
cmd = [
|
||||
"docker",
|
||||
"exec",
|
||||
"-e",
|
||||
f"MYSQL_PWD={password}",
|
||||
host,
|
||||
"mysqldump",
|
||||
"-h",
|
||||
"127.0.0.1",
|
||||
"-P",
|
||||
port,
|
||||
"-u",
|
||||
user,
|
||||
*dump_flags(db),
|
||||
]
|
||||
rc = write_dump(cmd, env, out)
|
||||
if rc == 0:
|
||||
sys.exit(0)
|
||||
sys.stderr.write(f"docker exec {host} mysqldump 失败,尝试其它方式…\n")
|
||||
|
||||
# 2) 宿主机 mysqldump
|
||||
if shutil.which("mysqldump"):
|
||||
cmd = [
|
||||
"mysqldump",
|
||||
"-h",
|
||||
host,
|
||||
"-P",
|
||||
port,
|
||||
"-u",
|
||||
user,
|
||||
*dump_flags(db),
|
||||
]
|
||||
rc = write_dump(cmd, env, out)
|
||||
if rc == 0:
|
||||
sys.exit(0)
|
||||
|
||||
# 3) 临时 mysql 客户端容器(1panel-network)
|
||||
for net in ("1panel-network", "bridge"):
|
||||
if not network_exists(net):
|
||||
continue
|
||||
cmd = [
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--network",
|
||||
net,
|
||||
"-e",
|
||||
f"MYSQL_PWD={password}",
|
||||
"mysql:8.0",
|
||||
"mysqldump",
|
||||
"-h",
|
||||
host,
|
||||
"-P",
|
||||
port,
|
||||
"-u",
|
||||
user,
|
||||
*dump_flags(db),
|
||||
]
|
||||
rc = write_dump(cmd, env, out)
|
||||
if rc == 0:
|
||||
sys.exit(0)
|
||||
|
||||
sys.stderr.write("所有 mysqldump 策略均失败(无宿主机客户端且 Docker 回退不可用)\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
@@ -0,0 +1,22 @@
|
||||
# 复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh
|
||||
# 用于 git push(scripts/git-push.sh);clone/pull 仍可匿名
|
||||
|
||||
# NEXUS_GITEA_HOST='axs.kuma1xn.vip' # HTTPS Gitea
|
||||
# NEXUS_GITEA_SCHEME='https'
|
||||
# NEXUS_GITEA_REPO='admin/Nexus.git'
|
||||
# admin 账号密码或 Access Token:
|
||||
# NEXUS_GITEA_TOKEN='你的密码或令牌'
|
||||
|
||||
# 新机安装勿填下面三项(install-nexus-fresh / --fresh 会自动按机生成)
|
||||
# NEXUS_SECRET_KEY=''
|
||||
# NEXUS_API_KEY=''
|
||||
# NEXUS_ENCRYPTION_KEY=''
|
||||
|
||||
NEXUS_DOMAIN='api.synaglobal.vip'
|
||||
NEXUS_ROOT='/opt/nexus'
|
||||
NEXUS_PROFILE='2c8g'
|
||||
NEXUS_FROM_ENV='/www/wwwroot/api.synaglobal.vip/.env'
|
||||
|
||||
# 本机生产 SSH(deploy-production.sh 读取;私钥路径勿提交真实 .pem)
|
||||
# NEXUS_SSH='azureuser@20.24.218.235'
|
||||
# NEXUS_SSH_KEY='/path/to/nz-admin.pem'
|
||||
Executable
+1359
File diff suppressed because it is too large
Load Diff
@@ -61,7 +61,7 @@ server {
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
location ~ /data/config\.php$ {
|
||||
location ~ /data/config\.json$ {
|
||||
deny all;
|
||||
}
|
||||
|
||||
@@ -75,5 +75,5 @@ server {
|
||||
error_log {{LOG_DIR}}/{{SERVER_NAME}}_nexus.error.log;
|
||||
|
||||
# ── Upload limit ──
|
||||
client_max_body_size 100m;
|
||||
client_max_body_size 500m;
|
||||
}
|
||||
|
||||
@@ -89,10 +89,16 @@ server {
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
location ~ /data/config\.php$ {
|
||||
location ~ /data/config\.json$ {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# ── Redirect bare page paths to /app/ (for reverse-proxy deployments) ──
|
||||
# If you use proxy_pass instead of root+try_files, uncomment this block:
|
||||
# location ~ ^/(login|index|servers|files|push|scripts|script-executions|credentials|schedules|retries|commands|alerts|audit|settings|install|terminal|assets)\.html$ {
|
||||
# return 301 /app/$1.html;
|
||||
# }
|
||||
|
||||
# ── Static HTML pages (no SPA — each page is standalone) ──
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
@@ -103,5 +109,5 @@ server {
|
||||
error_log {{LOG_DIR}}/{{SERVER_NAME}}.error.log;
|
||||
|
||||
# ── Upload limit ──
|
||||
client_max_body_size 100m;
|
||||
client_max_body_size 500m;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus NX — 统一运维菜单(安装 + 升级 + 日常运维)
|
||||
#
|
||||
# nx 交互菜单
|
||||
# nx update 一键更新(等同 deploy/update.sh)
|
||||
# nx update --no-cache 无缓存重建镜像
|
||||
# nx install-fresh 全新安装
|
||||
# nx health 健康检查
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
NX_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
NX_DIR="$(cd "$(dirname "$NX_SELF")" && pwd)"
|
||||
# shellcheck source=./nexus-1panel.sh
|
||||
source "${NX_DIR}/nexus-1panel.sh"
|
||||
|
||||
pause_enter() {
|
||||
echo ""
|
||||
read -r -p "按 Enter 继续..."
|
||||
}
|
||||
|
||||
clear_screen() {
|
||||
if [[ -t 1 ]]; then
|
||||
clear 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
is_stack_installed() {
|
||||
[[ -d "${NEXUS_ROOT}/.git" && -f "${NEXUS_ROOT}/${ENV_PROD}" ]]
|
||||
}
|
||||
|
||||
detect_runtime() {
|
||||
if is_stack_installed && docker compose version >/dev/null 2>&1; then
|
||||
local prof q
|
||||
prof="$(profile_env_file "$NEXUS_ROOT")"
|
||||
if [[ -f "$prof" && -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
||||
local -a det_args=(-f "$COMPOSE_FILE")
|
||||
if has_1panel_network && [[ -f "$NEXUS_ROOT/$COMPOSE_1PANEL" ]]; then
|
||||
det_args+=(-f "$COMPOSE_1PANEL")
|
||||
fi
|
||||
q="$(cd "$NEXUS_ROOT" && docker compose "${det_args[@]}" \
|
||||
--env-file "$ENV_PROD" --env-file "$prof" ps -q nexus 2>/dev/null || true)"
|
||||
if [[ -n "$q" ]]; then
|
||||
NX_RUNTIME="docker"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
NX_RUNTIME="supervisor"
|
||||
return 0
|
||||
fi
|
||||
NX_RUNTIME="none"
|
||||
}
|
||||
|
||||
install_nx_cli() {
|
||||
NEXUS_ROOT="$(cd "${NX_DIR}/.." && pwd)" \
|
||||
bash "${NX_DIR}/install-nx-cli.sh" 2>/dev/null || true
|
||||
}
|
||||
|
||||
menu_header() {
|
||||
local title="$1"
|
||||
clear_screen
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
printf "║ Nexus NX — %-44s║\n" "$title"
|
||||
echo "╠══════════════════════════════════════════════════════════╣"
|
||||
echo "║ 域名: ${NEXUS_DOMAIN} 目录: ${NEXUS_ROOT}"
|
||||
if is_stack_installed; then
|
||||
detect_runtime
|
||||
echo "║ 运行: ${NX_RUNTIME} 档位: $(cat "${NEXUS_ROOT}/${HOST_PROFILE_FILE}" 2>/dev/null || echo "${NEXUS_PROFILE}")"
|
||||
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
echo "║ 版本: $(git -C "${NEXUS_ROOT}" log -1 --oneline 2>/dev/null || echo '?')"
|
||||
fi
|
||||
else
|
||||
echo "║ 状态: 未安装"
|
||||
fi
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
}
|
||||
|
||||
run_install() {
|
||||
local profile="${1:-2c8g}" extra="${2:-}"
|
||||
NEXUS_PROFILE="$profile"
|
||||
SKIP_CLONE=false
|
||||
FRESH_INSTALL=false
|
||||
NO_BACKUP=false
|
||||
PRUNE_IMAGES=false
|
||||
FROM_ENV="${NEXUS_FROM_ENV}"
|
||||
case "$extra" in
|
||||
skip-clone) SKIP_CLONE=true ;;
|
||||
fresh) FRESH_INSTALL=true ;;
|
||||
from-env) FROM_ENV="${NEXUS_FROM_ENV}" ;;
|
||||
esac
|
||||
load_secrets
|
||||
cmd_install
|
||||
install_nx_cli
|
||||
pause_enter
|
||||
}
|
||||
|
||||
menu_install() {
|
||||
while true; do
|
||||
menu_header "安装"
|
||||
cat <<'EOF'
|
||||
[1] 一键安装(默认 2核8G / 2c8g)
|
||||
[2] 安装 — 4核16G(4c16g)
|
||||
[3] 安装 — 1核4G(1c4g)
|
||||
[4] 自定义规格(输入 CPU 与内存 GB)
|
||||
[5] 已 clone 到 /opt/nexus,仅 pull + 启动(--skip-clone)
|
||||
[6] 全新安装(install-nexus-fresh.sh → /app/install.html)
|
||||
[7] 检查端口 / 防火墙(不安装)
|
||||
[0] 返回
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) run_install 2c8g "" ;;
|
||||
2) run_install 4c16g "" ;;
|
||||
3) run_install 1c4g "" ;;
|
||||
4)
|
||||
read -r -p "CPU 核数: " cpus
|
||||
read -r -p "内存 GB: " mem
|
||||
CUSTOM_CPUS="$cpus"
|
||||
CUSTOM_MEM_GB="$mem"
|
||||
run_install 2c8g ""
|
||||
;;
|
||||
5) run_install 2c8g skip-clone ;;
|
||||
6)
|
||||
if [[ -x "${NX_DIR}/install-nexus-fresh.sh" ]]; then
|
||||
bash "${NX_DIR}/install-nexus-fresh.sh" --profile "${NEXUS_PROFILE:-2c8g}"
|
||||
else
|
||||
run_install 2c8g fresh
|
||||
fi
|
||||
;;
|
||||
7)
|
||||
require_root
|
||||
load_secrets
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
if is_stack_installed; then
|
||||
check_ports_post "$NEXUS_ROOT"
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
0) return 0 ;;
|
||||
*) warn "无效选项" ; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
ops_restart_nexus() {
|
||||
step "重启 Nexus(Python)..."
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" restart nexus
|
||||
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
|
||||
supervisorctl restart nexus
|
||||
else
|
||||
error "未检测到运行中的 Nexus"
|
||||
return 1
|
||||
fi
|
||||
sleep 3
|
||||
verify_health "$NEXUS_ROOT" "重启" || true
|
||||
}
|
||||
|
||||
ops_restart_stack() {
|
||||
step "重启 Compose 栈..."
|
||||
compose_cmd "$NEXUS_ROOT" restart
|
||||
sleep 5
|
||||
verify_health "$NEXUS_ROOT" "重启" || true
|
||||
}
|
||||
|
||||
ops_stop_stack() {
|
||||
warn "将停止 Nexus 容器(MySQL/Redis 为外置服务,不受影响)"
|
||||
read -r -p "确认? [y/N]: " c
|
||||
[[ "$c" == "y" || "$c" == "Y" ]] || return 0
|
||||
compose_cmd "$NEXUS_ROOT" stop
|
||||
info "已停止"
|
||||
}
|
||||
|
||||
ops_start_stack() {
|
||||
compose_cmd "$NEXUS_ROOT" up -d --remove-orphans
|
||||
verify_nexus_1panel_network "$NEXUS_ROOT" || return 1
|
||||
verify_health "$NEXUS_ROOT" "启动" || true
|
||||
}
|
||||
|
||||
ops_logs() {
|
||||
local svc="${1:-nexus}"
|
||||
echo ""
|
||||
read -r -p "跟踪日志行数 [80]: " n
|
||||
n="${n:-80}"
|
||||
compose_cmd "$NEXUS_ROOT" logs --tail="$n" -f "$svc"
|
||||
}
|
||||
|
||||
ops_status() {
|
||||
if [[ "$NX_RUNTIME" == "docker" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" ps
|
||||
elif [[ "$NX_RUNTIME" == "supervisor" ]]; then
|
||||
supervisorctl status nexus
|
||||
fi
|
||||
echo ""
|
||||
local port h code
|
||||
port="$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
h=$(curl -sf "http://127.0.0.1:${port}/health" 2>/dev/null || echo "fail")
|
||||
info "/health → ${h}"
|
||||
local cname
|
||||
cname="$(nexus_container_name 2>/dev/null || true)"
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/install.html" 2>/dev/null || echo "000")
|
||||
if [[ -n "$cname" ]] && nexus_container_has_install_lock "$cname"; then
|
||||
if [[ "$code" == "404" ]]; then
|
||||
info "/app/install.html → 已归档 (HTTP 404)"
|
||||
else
|
||||
warn "/app/install.html → 应已归档但 HTTP ${code}"
|
||||
fi
|
||||
else
|
||||
info "/app/install.html → HTTP ${code}"
|
||||
fi
|
||||
}
|
||||
|
||||
ops_backup_now() {
|
||||
NO_BACKUP=false
|
||||
backup_mysql "$NEXUS_ROOT"
|
||||
}
|
||||
|
||||
ops_install_cron() {
|
||||
bash "${NX_DIR}/install_ops_cron.sh"
|
||||
}
|
||||
|
||||
ops_update() {
|
||||
local args=()
|
||||
PRUNE_IMAGES=false
|
||||
NO_BACKUP=false
|
||||
NO_CACHE=false
|
||||
read -r -p "跳过 MySQL 备份? [y/N]: " nb
|
||||
[[ "$nb" == "y" || "$nb" == "Y" ]] && args+=(--no-backup)
|
||||
read -r -p "无缓存重建镜像? [y/N]: " nc
|
||||
[[ "$nc" == "y" || "$nc" == "Y" ]] && args+=(--no-cache)
|
||||
read -r -p "升级后清理悬空镜像? [y/N]: " pr
|
||||
[[ "$pr" == "y" || "$pr" == "Y" ]] && args+=(--prune)
|
||||
bash "${NX_DIR}/update.sh" "${args[@]}"
|
||||
pause_enter
|
||||
}
|
||||
|
||||
require_installed() {
|
||||
if ! is_stack_installed; then
|
||||
warn "尚未安装,请先使用菜单 [1] 安装"
|
||||
pause_enter
|
||||
return 1
|
||||
fi
|
||||
require_root
|
||||
load_secrets
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
detect_runtime
|
||||
return 0
|
||||
}
|
||||
|
||||
menu_main() {
|
||||
load_secrets
|
||||
while true; do
|
||||
menu_header "运维菜单"
|
||||
cat <<'EOF'
|
||||
── 安装 ──
|
||||
[1] 安装向导(新机 / 迁机 / 档位)
|
||||
[2] 全新安装 → /app/install.html
|
||||
|
||||
── 日常运维(需已安装)──
|
||||
[3] 状态 + 健康检查
|
||||
[4] 一键更新(pull + 重建镜像)★
|
||||
[5] 重启 Nexus(Python)
|
||||
[6] 查看 Nexus 日志
|
||||
[7] 启动 / 停止 Compose 栈
|
||||
[8] 重建镜像并启动(--build nexus)
|
||||
[9] 备份 MySQL
|
||||
[e] 安装巡检/备份 cron(health_monitor + db_backup)
|
||||
[v] 1Panel 安装/运维验收脚本
|
||||
|
||||
── 其它 ──
|
||||
[a] 检查 Git 是否有更新
|
||||
[b] 端口 / 防火墙检查
|
||||
[c] 进入 Nexus 容器 shell
|
||||
[d] 1Panel 反代说明
|
||||
[0] 退出
|
||||
|
||||
EOF
|
||||
read -r -p "请选择: " choice
|
||||
case "$choice" in
|
||||
1) menu_install ;;
|
||||
2)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
warn "将执行全新安装(清除 nexus-state 卷内旧配置并重建容器)"
|
||||
read -r -p "确认继续? [y/N]: " fresh_ok
|
||||
[[ "$fresh_ok" == "y" || "$fresh_ok" == "Y" ]] || continue
|
||||
fi
|
||||
exec bash "${NX_DIR}/install-nexus-fresh.sh" --profile "${NEXUS_PROFILE:-2c8g}"
|
||||
;;
|
||||
3)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
load_saved_profile "$NEXUS_ROOT"
|
||||
detect_runtime
|
||||
ops_status
|
||||
else
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
fi
|
||||
pause_enter
|
||||
;;
|
||||
4)
|
||||
require_installed || continue
|
||||
ops_update
|
||||
;;
|
||||
5)
|
||||
require_installed || continue
|
||||
ops_restart_nexus
|
||||
pause_enter
|
||||
;;
|
||||
6)
|
||||
require_installed || continue
|
||||
ops_logs nexus
|
||||
;;
|
||||
7)
|
||||
require_installed || continue
|
||||
echo " [1] 启动 [2] 停止 [3] 重启栈"
|
||||
read -r -p "选: " s
|
||||
case "$s" in
|
||||
1) ops_start_stack ;;
|
||||
2) ops_stop_stack ;;
|
||||
3) ops_restart_stack ;;
|
||||
*) warn "无效" ;;
|
||||
esac
|
||||
pause_enter
|
||||
;;
|
||||
8)
|
||||
require_installed || continue
|
||||
read -r -p "无缓存重建? [y/N]: " nc
|
||||
if [[ "$nc" == "y" || "$nc" == "Y" ]]; then
|
||||
compose_cmd "$NEXUS_ROOT" build --no-cache nexus
|
||||
fi
|
||||
compose_cmd "$NEXUS_ROOT" up -d --build --remove-orphans nexus
|
||||
verify_nexus_1panel_network "$NEXUS_ROOT" || true
|
||||
verify_health "$NEXUS_ROOT" "重建" || true
|
||||
pause_enter
|
||||
;;
|
||||
9)
|
||||
require_installed || continue
|
||||
ops_backup_now
|
||||
pause_enter
|
||||
;;
|
||||
e|E)
|
||||
require_root
|
||||
ops_install_cron
|
||||
pause_enter
|
||||
;;
|
||||
v|V)
|
||||
require_root
|
||||
bash "${NX_DIR}/verify-1panel-install-wizard.sh"
|
||||
pause_enter
|
||||
;;
|
||||
a|A)
|
||||
require_root
|
||||
cmd_check
|
||||
pause_enter
|
||||
;;
|
||||
b|B)
|
||||
require_root
|
||||
check_ports_preflight "$NEXUS_ROOT"
|
||||
is_stack_installed && check_ports_post "$NEXUS_ROOT" || true
|
||||
pause_enter
|
||||
;;
|
||||
c|C)
|
||||
require_installed || continue
|
||||
compose_cmd "$NEXUS_ROOT" exec nexus bash || compose_cmd "$NEXUS_ROOT" exec nexus sh
|
||||
;;
|
||||
d|D)
|
||||
info "1Panel → 网站 → 创建站点 ${NEXUS_DOMAIN}"
|
||||
info "反向代理: http://127.0.0.1:$(nexus_publish_port "$NEXUS_ROOT")"
|
||||
info "示例: ${NEXUS_ROOT}/deploy/1panel/openresty-nexus.conf.example"
|
||||
pause_enter
|
||||
;;
|
||||
0) exit 0 ;;
|
||||
*) warn "无效选项"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
nx_main() {
|
||||
local sub="${1:-}"
|
||||
install_nx_cli
|
||||
load_secrets
|
||||
case "$sub" in
|
||||
""|menu|god|ops|admin)
|
||||
# god/ops 已合并进主菜单,保留别名兼容
|
||||
[[ "$sub" == "god" || "$sub" == "ops" || "$sub" == "admin" ]] && \
|
||||
warn "「上帝菜单」已合并进 nx 主菜单,直接选对应项即可"
|
||||
menu_main
|
||||
;;
|
||||
install) menu_install ;;
|
||||
update|upgrade)
|
||||
require_root
|
||||
shift || true
|
||||
exec bash "${NX_DIR}/update.sh" "$@"
|
||||
;;
|
||||
install-fresh|fresh)
|
||||
require_root
|
||||
exec bash "${NX_DIR}/install-nexus-fresh.sh" "$@"
|
||||
;;
|
||||
install-auto|auto)
|
||||
require_root
|
||||
if is_stack_installed; then
|
||||
warn "已安装,install-auto 将执行升级而非重复安装"
|
||||
exec bash "${NX_DIR}/update.sh"
|
||||
else
|
||||
run_install "${NEXUS_PROFILE:-2c8g}" ""
|
||||
fi
|
||||
;;
|
||||
health)
|
||||
require_root
|
||||
is_stack_installed && ops_status || check_ports_preflight "$NEXUS_ROOT"
|
||||
;;
|
||||
cron)
|
||||
require_root
|
||||
ops_install_cron
|
||||
;;
|
||||
verify)
|
||||
require_root
|
||||
bash "${NX_DIR}/verify-1panel-install-wizard.sh"
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
用法: nx [子命令]
|
||||
|
||||
(无) 统一运维菜单(安装 + 升级 + 重启 + 日志)
|
||||
update 一键更新(等同 bash deploy/update.sh)
|
||||
install-fresh 全新安装(/app/install.html)
|
||||
install-auto 非交互安装(默认 2c8g)
|
||||
health 健康检查
|
||||
cron 安装 health_monitor + db_backup crontab
|
||||
verify 运行 1Panel 验收脚本
|
||||
|
||||
god / ops 已合并进主菜单(兼容别名)
|
||||
|
||||
curl 入口:
|
||||
安装 curl -fsSL .../deploy/quick-install.sh | bash
|
||||
更新 curl -fsSL .../deploy/update.sh | bash
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
main "$@"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
chmod +x "${NX_DIR}/nx" 2>/dev/null || true
|
||||
nx_main "$@"
|
||||
@@ -0,0 +1,362 @@
|
||||
#!/bin/bash
|
||||
# Nexus Pre-deploy Gate Check (v3 — 8 gates)
|
||||
# 在部署前强制检查 8 道门控,任何一道不过则阻止部署
|
||||
#
|
||||
# 门控1: CHANGELOG — docs/changelog/ 下必须有今天的 .md 文件(且行数≥10)
|
||||
# 门控2: AUDIT — docs/audit/ 下必须有今天的审计记录(且包含关键段落)
|
||||
# 门控3: TEST — tests/test_api.py 必须存在且全通过
|
||||
# 门控4: LINT — ruff check server/ 零错误
|
||||
# 门控5: IMPORT — python -c "import server.main" 成功
|
||||
# 门控6: SECURITY — bandit -r server/ 无 HIGH/MEDIUM
|
||||
# 门控7: REVIEW — 审计文件包含 Closure表+文件清单+DoD,且文件清单与 git diff 交叉验证
|
||||
# 门控8: AI_REVIEW — cursor-agent 审查 HEAD~1..HEAD;缓存 docs/reviews/<head_sha>.json
|
||||
#
|
||||
# 用法: bash deploy/pre_deploy_check.sh
|
||||
# 退出码: 0=全部通过, 1=至少一道门未过
|
||||
# 日志: deploy/gate_log.jsonl(每次检查自动追加记录)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
_REPO_ROOT="$(cd "${_SCRIPT_DIR}/.." && pwd)"
|
||||
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
TIMESTAMP=$(date -Iseconds)
|
||||
GATES_PASSED=0
|
||||
GATES_TOTAL=8
|
||||
BLOCKED=0
|
||||
GATE_RESULTS=""
|
||||
|
||||
# Resolve tool from .venv / venv / PATH
|
||||
_pick_bin() {
|
||||
local name="$1"
|
||||
for p in "${DEPLOY_DIR}/.venv/bin/${name}" "${DEPLOY_DIR}/venv/bin/${name}"; do
|
||||
if [ -x "${p}" ]; then
|
||||
echo "${p}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
command -v "${name}" 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# ── 日志函数 ──
|
||||
gate_log() {
|
||||
local gate="$1" result="$2" detail="${3:-}"
|
||||
local entry="{\"ts\":\"${TIMESTAMP}\",\"date\":\"${TODAY}\",\"gate\":\"${gate}\",\"result\":\"${result}\",\"detail\":\"${detail}\"}"
|
||||
GATE_RESULTS="${GATE_RESULTS}${entry},"
|
||||
# 追加到 JSONL 日志文件
|
||||
echo "${entry}" >> "${DEPLOY_DIR}/deploy/gate_log.jsonl"
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo " Nexus Pre-deploy Gate Check (v3)"
|
||||
echo " Date: ${TODAY}"
|
||||
echo " Gates: ${GATES_TOTAL}"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# ── Gate 1: Changelog (存在 + 行数≥10) ──
|
||||
echo -n "Gate 1/8: Changelog ... "
|
||||
CHANGELOG_DIR="${DEPLOY_DIR}/docs/changelog"
|
||||
if ls "${CHANGELOG_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${CHANGELOG_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
LINES=$(wc -l < "${FILE}")
|
||||
if [ "${LINES}" -ge 10 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ Found: $(basename "${FILE}") (${LINES} lines)"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "changelog" "PASS" "$(basename "${FILE}") ${LINES}lines"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ File exists but only ${LINES} lines (need ≥10)"
|
||||
echo " └─ Changelog must have substance, not just a placeholder"
|
||||
BLOCKED=1
|
||||
gate_log "changelog" "BLOCK" "only ${LINES} lines"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ No changelog file found: ${CHANGELOG_DIR}/${TODAY}-*.md"
|
||||
echo " └─ Create changelog before deploying"
|
||||
BLOCKED=1
|
||||
gate_log "changelog" "BLOCK" "file not found"
|
||||
fi
|
||||
|
||||
# ── Gate 2: Audit (存在 + 关键段落) ──
|
||||
echo -n "Gate 2/8: Audit ... "
|
||||
AUDIT_DIR="${DEPLOY_DIR}/docs/audit"
|
||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
# 检查关键段落是否存在
|
||||
HAS_STEP3=$(grep -c "Step 3" "${FILE}" 2>/dev/null || echo 0)
|
||||
HAS_CLOSURE=$(grep -c "Closure" "${FILE}" 2>/dev/null || echo 0)
|
||||
HAS_DOD=$(grep -c "DoD" "${FILE}" 2>/dev/null || echo 0)
|
||||
if [ "${HAS_STEP3}" -ge 1 ] && [ "${HAS_CLOSURE}" -ge 1 ] && [ "${HAS_DOD}" -ge 1 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ Found: $(basename "${FILE}") (Step3✓ Closure✓ DoD✓)"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "audit" "PASS" "$(basename "${FILE}")"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Audit file exists but missing required sections:"
|
||||
[ "${HAS_STEP3}" -eq 0 ] && echo " └─ Missing: Step 3 (规则扫描)"
|
||||
[ "${HAS_CLOSURE}" -eq 0 ] && echo " └─ Missing: Closure表"
|
||||
[ "${HAS_DOD}" -eq 0 ] && echo " └─ Missing: DoD (Definition of Done)"
|
||||
BLOCKED=1
|
||||
gate_log "audit" "BLOCK" "missing sections"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ No audit file found: ${AUDIT_DIR}/${TODAY}-*.md"
|
||||
echo " └─ Run line-walk audit (8 steps) and save report before deploying"
|
||||
BLOCKED=1
|
||||
gate_log "audit" "BLOCK" "file not found"
|
||||
fi
|
||||
|
||||
# ── Gate 3: Test (必须存在 + 必须通过) ──
|
||||
echo -n "Gate 3/8: Test ... "
|
||||
TEST_SCRIPT="${DEPLOY_DIR}/tests/test_api.py"
|
||||
if [ ! -f "${TEST_SCRIPT}" ]; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Test script not found: ${TEST_SCRIPT}"
|
||||
echo " └─ Tests are MANDATORY — cannot deploy without test coverage"
|
||||
BLOCKED=1
|
||||
gate_log "test" "BLOCK" "script not found"
|
||||
else
|
||||
# Always hit local API for gate 3 (ignore shell NEXUS_TEST_BASE used by import/e2e scripts)
|
||||
TEST_OUTPUT=$(cd "${DEPLOY_DIR}" && env -u NEXUS_TEST_BASE NEXUS_TEST_BASE=http://127.0.0.1:8600 python3 "${TEST_SCRIPT}" 2>&1) || true
|
||||
FAIL_COUNT=$(echo "${TEST_OUTPUT}" | grep -cE "^\s+\[FAIL\]" 2>/dev/null || true)
|
||||
FAIL_COUNT=${FAIL_COUNT:-0}
|
||||
GATE3_PASSED=0
|
||||
if [ "${FAIL_COUNT}" -gt 0 ] || echo "${TEST_OUTPUT}" | grep -qE "[0-9]+ test(s) failed"; then
|
||||
# 本机未起 :8600 时,若失败均为 Connection refused,回退 pytest 专项
|
||||
NON_CONN_FAILS=$(echo "${TEST_OUTPUT}" | grep -E "^\s+\[FAIL\]" | grep -vcE "Connection refused|Errno 111" 2>/dev/null || true)
|
||||
NON_CONN_FAILS=${NON_CONN_FAILS:-0}
|
||||
if [ "${FAIL_COUNT}" -gt 0 ] && [ "${NON_CONN_FAILS}" -eq 0 ]; then
|
||||
PYTEST_BIN=$(_pick_bin pytest || true)
|
||||
FALLBACK_TESTS="tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py tests/test_gate_ai_review.py"
|
||||
if [ -n "${PYTEST_BIN}" ]; then
|
||||
echo -e "${YELLOW}fallback${NC}"
|
||||
echo " └─ Local :8600 unreachable — running pytest gate batch"
|
||||
FALLBACK_OUTPUT=$(cd "${DEPLOY_DIR}" && "${PYTEST_BIN}" ${FALLBACK_TESTS} -q 2>&1) || FALLBACK_RC=$?
|
||||
FALLBACK_RC=${FALLBACK_RC:-0}
|
||||
if [ "${FALLBACK_RC}" -eq 0 ]; then
|
||||
echo -e " └─ ${GREEN}PASS${NC} (pytest fallback)"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "test" "PASS" "pytest fallback :8600 down"
|
||||
GATE3_PASSED=1
|
||||
else
|
||||
echo -e " └─ ${RED}BLOCK${NC} pytest fallback failed:"
|
||||
echo "${FALLBACK_OUTPUT}" | tail -15 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "test" "BLOCK" "pytest fallback failed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "${GATE3_PASSED}" -eq 0 ]; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Tests failed. Output:"
|
||||
echo "${TEST_OUTPUT}" | head -20 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "test" "BLOCK" "tests failed"
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ All tests passed"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "test" "PASS" "all passed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Gate 4: Lint (ruff — F only: undefined names, unused imports) ──
|
||||
echo -n "Gate 4/8: Lint ... "
|
||||
RUFF_BIN=$(_pick_bin ruff || true)
|
||||
if [ -n "${RUFF_BIN}" ]; then
|
||||
LINT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${RUFF_BIN}" check server/ --select F 2>&1) || true
|
||||
LINT_COUNT=$(echo "${LINT_OUTPUT}" | grep -cE "^server/" 2>/dev/null || true)
|
||||
LINT_COUNT=$(echo "${LINT_COUNT}" | head -1 | tr -d '[:space:]')
|
||||
LINT_COUNT=${LINT_COUNT:-0}
|
||||
if [ "${LINT_COUNT}" -eq 0 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ ruff check: 0 violations"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "lint" "PASS" "0 violations"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ ruff found ${LINT_COUNT} violations:"
|
||||
echo "${LINT_OUTPUT}" | head -15 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "lint" "BLOCK" "${LINT_COUNT} violations"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ ruff not found (.venv/bin/ruff or pip install ruff)"
|
||||
BLOCKED=1
|
||||
gate_log "lint" "BLOCK" "ruff not found"
|
||||
fi
|
||||
|
||||
# ── Gate 5: Import (python -c "import server.main") ──
|
||||
echo -n "Gate 5/8: Import ... "
|
||||
IMPORT_PYTHON=""
|
||||
for p in "${DEPLOY_DIR}/.venv/bin/python3" "${DEPLOY_DIR}/venv/bin/python3"; do
|
||||
if [ -x "${p}" ]; then
|
||||
IMPORT_PYTHON="${p}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "${IMPORT_PYTHON}" ]; then
|
||||
IMPORT_PYTHON="python3"
|
||||
fi
|
||||
IMPORT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${IMPORT_PYTHON}" -c "import server.main" 2>&1) && IMPORT_OK=1 || IMPORT_OK=0
|
||||
if [ "${IMPORT_OK}" -eq 1 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ server.main imports successfully"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "import" "PASS" "ok"
|
||||
else
|
||||
# 检查是否有真正的导入错误(不是缺少运行时依赖如数据库)
|
||||
if echo "${IMPORT_OUTPUT}" | grep -qiE "ImportError|ModuleNotFoundError|SyntaxError"; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Import failed:"
|
||||
echo "${IMPORT_OUTPUT}" | head -5 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "import" "BLOCK" "import/syntax error"
|
||||
else
|
||||
# 运行时依赖缺失(如数据库连接)不算BLOCK,但警告
|
||||
echo -e "${YELLOW}WARN${NC}"
|
||||
echo " └─ Import has runtime dependency issues (not code errors)"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "import" "WARN" "runtime deps only"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Gate 6: Security (bandit — HIGH only) ──
|
||||
echo -n "Gate 6/8: Security ... "
|
||||
BANDIT_BIN=$(_pick_bin bandit || true)
|
||||
if [ -n "${BANDIT_BIN}" ]; then
|
||||
SEC_OUTPUT=$(cd "${DEPLOY_DIR}" && "${BANDIT_BIN}" -r server/ -f txt -ll 2>&1) || true
|
||||
HIGH_COUNT=$(echo "${SEC_OUTPUT}" | grep -cE "Severity: High" 2>/dev/null || true)
|
||||
HIGH_COUNT=$(echo "${HIGH_COUNT}" | head -1 | tr -d '[:space:]')
|
||||
HIGH_COUNT=${HIGH_COUNT:-0}
|
||||
if [ "${HIGH_COUNT}" -eq 0 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ bandit: 0 HIGH findings"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "security" "PASS" "0 HIGH"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ bandit found ${HIGH_COUNT} HIGH severity issues:"
|
||||
echo "${SEC_OUTPUT}" | grep -A3 "Severity: High" | head -15 | sed 's/^/ │ /'
|
||||
BLOCKED=1
|
||||
gate_log "security" "BLOCK" "${HIGH_COUNT} HIGH findings"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ bandit not found (.venv/bin/bandit or pip install bandit)"
|
||||
BLOCKED=1
|
||||
gate_log "security" "BLOCK" "bandit not found"
|
||||
fi
|
||||
|
||||
# ── Gate 7: Review (审计文件交叉验证) ──
|
||||
echo -n "Gate 7/8: Review ... "
|
||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
# 检查审计文件中是否列出了实际改动的文件
|
||||
REVIEW_OK=1
|
||||
# 如果是git仓库,交叉验证改动文件
|
||||
if command -v git &>/dev/null && [ -d "${DEPLOY_DIR}/.git" ]; then
|
||||
CHANGED_FILES=$(cd "${DEPLOY_DIR}" && git diff --name-only HEAD~1..HEAD 2>/dev/null || true)
|
||||
for CF in ${CHANGED_FILES}; do
|
||||
# 跳过非代码文件与门控运行日志
|
||||
case "${CF}" in
|
||||
docs/*|CLAUDE.md|*.md|deploy/gate_log.jsonl|web/app/assets/*) continue ;;
|
||||
esac
|
||||
# 检查审计文件是否提到了这个文件
|
||||
if ! grep -q "$(basename "${CF}")" "${FILE}" 2>/dev/null; then
|
||||
if [ "${REVIEW_OK}" -eq 1 ]; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
REVIEW_OK=0
|
||||
fi
|
||||
echo " └─ Changed file not in audit: ${CF}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ "${REVIEW_OK}" -eq 1 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " └─ Audit covers all changed files"
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
gate_log "review" "PASS" "audit covers changes"
|
||||
else
|
||||
BLOCKED=1
|
||||
gate_log "review" "BLOCK" "audit missing changed files"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ No audit file to validate against"
|
||||
BLOCKED=1
|
||||
gate_log "review" "BLOCK" "no audit file"
|
||||
fi
|
||||
|
||||
# ── Gate 8: AI Review (cursor-agent + docs/reviews cache) ──
|
||||
echo -n "Gate 8/8: AI Review ... "
|
||||
AI_REVIEW_OUT=$(cd "${DEPLOY_DIR}" && python3 "${DEPLOY_DIR}/scripts/gate_ai_review.py" 2>&1) || AI_REVIEW_RC=$?
|
||||
AI_REVIEW_RC=${AI_REVIEW_RC:-0}
|
||||
if [ "${AI_REVIEW_RC}" -eq 0 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo "${AI_REVIEW_OUT}" | sed 's/^/ └─ /' | head -5
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
if echo "${AI_REVIEW_OUT}" | grep -qi "skip"; then
|
||||
gate_log "ai_review" "PASS" "skipped no code"
|
||||
else
|
||||
gate_log "ai_review" "PASS" "ok"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo "${AI_REVIEW_OUT}" | sed 's/^/ │ /' | head -12
|
||||
BLOCKED=1
|
||||
gate_log "ai_review" "BLOCK" "see gate_ai_review.py output"
|
||||
fi
|
||||
|
||||
# ── Pre-flight: shell scripts LF ──
|
||||
echo -n "Pre-flight: Shell EOL (LF) ... "
|
||||
if bash "${DEPLOY_DIR}/deploy/check_shell_eol.sh" >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
gate_log "shell_eol" "PASS" "all tracked sh LF"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Run: bash deploy/check_shell_eol.sh"
|
||||
echo " └─ Fix: git add --renormalize '*.sh'"
|
||||
BLOCKED=1
|
||||
gate_log "shell_eol" "BLOCK" "CR in tracked sh"
|
||||
fi
|
||||
|
||||
# ── Pre-flight: deploy-critical paths LF in git index ──
|
||||
echo -n "Pre-flight: Text EOL (git index) ... "
|
||||
if python3 "${DEPLOY_DIR}/scripts/check_text_eol.py" >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
gate_log "text_eol" "PASS" "git index LF"
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo " └─ Run: python3 scripts/check_text_eol.py --fix-hint"
|
||||
BLOCKED=1
|
||||
gate_log "text_eol" "BLOCK" "CR in git index"
|
||||
fi
|
||||
|
||||
# ── Summary ──
|
||||
echo ""
|
||||
echo "========================================"
|
||||
if [ "${BLOCKED}" -eq 0 ]; then
|
||||
echo -e " Result: ${GREEN}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy allowed"
|
||||
echo "========================================"
|
||||
gate_log "summary" "PASS" "${GATES_PASSED}/${GATES_TOTAL}"
|
||||
exit 0
|
||||
else
|
||||
echo -e " Result: ${RED}${GATES_PASSED}/${GATES_TOTAL} gates passed${NC} — Deploy BLOCKED"
|
||||
echo "========================================"
|
||||
gate_log "summary" "BLOCK" "${GATES_PASSED}/${GATES_TOTAL}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus production host preflight before applying a release tarball.
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/preflight-release-host.sh \
|
||||
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
||||
# --sha256 a4eb8f35f0b98d40c1ad83117bfc652e7686ffc7d55f6bf90fcfd433a2ad6701 \
|
||||
# --deploy-path /opt/nexus
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TARBALL=""
|
||||
EXPECTED_SHA256=""
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
||||
MIN_FREE_MB="${NEXUS_RELEASE_MIN_FREE_MB:-1024}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
usage() {
|
||||
sed -n '1,12p' "$0"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--tarball)
|
||||
TARBALL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--sha256)
|
||||
EXPECTED_SHA256="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--deploy-path)
|
||||
DEPLOY_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--min-free-mb)
|
||||
MIN_FREE_MB="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error "Unknown argument: $1"
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
check_cmd() {
|
||||
local cmd="$1"
|
||||
if command -v "$cmd" >/dev/null 2>&1; then
|
||||
info "command OK: $cmd"
|
||||
return 0
|
||||
fi
|
||||
error "missing command: $cmd"
|
||||
return 1
|
||||
}
|
||||
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
detect_runtime() {
|
||||
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
|
||||
echo docker
|
||||
return
|
||||
fi
|
||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||
echo supervisor
|
||||
return
|
||||
fi
|
||||
echo unknown
|
||||
}
|
||||
|
||||
verify_tarball() {
|
||||
if [[ -z "$TARBALL" ]]; then
|
||||
warn "--tarball not provided; skipping tarball checksum/content checks"
|
||||
return 0
|
||||
fi
|
||||
[[ -f "$TARBALL" ]] || {
|
||||
error "tarball not found: $TARBALL"
|
||||
return 1
|
||||
}
|
||||
info "tarball exists: $TARBALL"
|
||||
if [[ -n "$EXPECTED_SHA256" ]]; then
|
||||
local actual
|
||||
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
|
||||
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
|
||||
error "SHA256 mismatch: expected=$EXPECTED_SHA256 actual=$actual"
|
||||
return 1
|
||||
fi
|
||||
info "SHA256 OK: $actual"
|
||||
else
|
||||
warn "--sha256 not provided; checksum not verified"
|
||||
fi
|
||||
tar tzf "$TARBALL" nexus-release/server nexus-release/deploy nexus-release/web/app-v2 >/dev/null
|
||||
info "tarball required paths OK"
|
||||
}
|
||||
|
||||
check_deploy_path() {
|
||||
if [[ -d "$DEPLOY_PATH" ]]; then
|
||||
info "deploy path exists: $DEPLOY_PATH"
|
||||
else
|
||||
warn "deploy path does not exist yet: $DEPLOY_PATH"
|
||||
fi
|
||||
if [[ -f "${DEPLOY_PATH}/.env" ]]; then
|
||||
info "runtime .env preserved path exists"
|
||||
else
|
||||
warn "${DEPLOY_PATH}/.env not found"
|
||||
fi
|
||||
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]]; then
|
||||
info "docker/.env.prod exists"
|
||||
else
|
||||
warn "${DEPLOY_PATH}/docker/.env.prod not found"
|
||||
fi
|
||||
if [[ -d "${DEPLOY_PATH}/web/data" ]]; then
|
||||
info "web/data exists"
|
||||
else
|
||||
warn "${DEPLOY_PATH}/web/data not found"
|
||||
fi
|
||||
}
|
||||
|
||||
check_disk_space() {
|
||||
local target free_mb
|
||||
target="$DEPLOY_PATH"
|
||||
[[ -e "$target" ]] || target="$(dirname "$DEPLOY_PATH")"
|
||||
free_mb="$(df -Pm "$target" | awk 'NR==2 {print $4}')"
|
||||
if [[ -z "$free_mb" ]]; then
|
||||
warn "unable to determine free disk space for $target"
|
||||
return 0
|
||||
fi
|
||||
if (( free_mb < MIN_FREE_MB )); then
|
||||
error "low disk space on $target: ${free_mb}MB < ${MIN_FREE_MB}MB"
|
||||
return 1
|
||||
fi
|
||||
info "disk free OK: ${free_mb}MB >= ${MIN_FREE_MB}MB"
|
||||
}
|
||||
|
||||
check_runtime() {
|
||||
local runtime
|
||||
runtime="$(detect_runtime)"
|
||||
info "runtime detected: $runtime"
|
||||
case "$runtime" in
|
||||
docker)
|
||||
docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' >/dev/null \
|
||||
&& info "Nexus docker container found" \
|
||||
|| warn "Nexus docker container not found by known name pattern"
|
||||
;;
|
||||
supervisor)
|
||||
supervisorctl status nexus || true
|
||||
;;
|
||||
*)
|
||||
warn "runtime unknown; apply script may require manual restart"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_local_endpoint() {
|
||||
local port health app app_v2
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
port="${port:-8600}"
|
||||
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
||||
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
|
||||
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
|
||||
echo " current /health -> ${health:-<empty>}"
|
||||
echo " current /app/ -> ${app}"
|
||||
echo " current /app-v2/ -> ${app_v2}"
|
||||
}
|
||||
|
||||
main() {
|
||||
local failures=0
|
||||
for cmd in tar sha256sum df awk grep sed curl; do
|
||||
check_cmd "$cmd" || failures=$((failures + 1))
|
||||
done
|
||||
check_cmd rsync || failures=$((failures + 1))
|
||||
|
||||
verify_tarball || failures=$((failures + 1))
|
||||
check_deploy_path
|
||||
check_disk_space || failures=$((failures + 1))
|
||||
check_runtime
|
||||
check_local_endpoint
|
||||
|
||||
if (( failures > 0 )); then
|
||||
error "preflight failed: ${failures} blocking issue(s)"
|
||||
return 1
|
||||
fi
|
||||
info "preflight OK"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove orphaned files under web/app/assets/ not referenced by current Vite build.
|
||||
|
||||
Vite/Rolldown minified bundles reference chunks by bare filename (e.g. ServersPage-xxx.js)
|
||||
without import() paths — scan file contents for known asset basenames.
|
||||
|
||||
Orphans newer than --retention-days are kept so long-lived browser tabs can still load
|
||||
their lazy chunks after a deploy (see docs/design/specs/2026-06-02-keep-session-no-refresh-design.md).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# Leading `_` required for Vite chunks like `_plugin-vue_export-helper-*.js`
|
||||
ASSET_NAME_RE = re.compile(
|
||||
r"[_A-Za-z0-9][A-Za-z0-9_.-]*\.(?:js|css|woff2?|ttf|eot)"
|
||||
)
|
||||
URL_RE = re.compile(r"""url\(["']?([^"')]+)["']?\)""")
|
||||
DEFAULT_RETENTION_DAYS = 14
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> tuple[bool, int, Path]:
|
||||
dry_run = "--dry-run" in argv
|
||||
retention_days = DEFAULT_RETENTION_DAYS
|
||||
positional: list[str] = []
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
arg = argv[i]
|
||||
if arg == "--dry-run":
|
||||
i += 1
|
||||
continue
|
||||
if arg.startswith("--retention-days="):
|
||||
retention_days = int(arg.split("=", 1)[1])
|
||||
i += 1
|
||||
continue
|
||||
if arg == "--retention-days":
|
||||
if i + 1 >= len(argv):
|
||||
raise SystemExit("--retention-days requires a value")
|
||||
retention_days = int(argv[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
positional.append(arg)
|
||||
i += 1
|
||||
if retention_days < 0:
|
||||
raise SystemExit("--retention-days must be >= 0")
|
||||
app_dir = Path(positional[0] if positional else "/www/wwwroot/api.synaglobal.vip/web/app")
|
||||
return dry_run, retention_days, app_dir
|
||||
|
||||
|
||||
def _referenced_basenames(text: str, known: set[str]) -> set[str]:
|
||||
found: set[str] = set()
|
||||
for m in ASSET_NAME_RE.finditer(text):
|
||||
name = m.group(0)
|
||||
if name in known:
|
||||
found.add(name)
|
||||
for raw in URL_RE.findall(text):
|
||||
name = Path(raw).name
|
||||
if name in known:
|
||||
found.add(name)
|
||||
return found
|
||||
|
||||
|
||||
def collect(app_dir: Path) -> set[Path]:
|
||||
assets = app_dir / "assets"
|
||||
index = app_dir / "index.html"
|
||||
if not index.is_file():
|
||||
raise SystemExit(f"missing {index}")
|
||||
|
||||
all_names = {p.name for p in assets.iterdir() if p.is_file()}
|
||||
keep_names: set[str] = set()
|
||||
|
||||
for m in re.finditer(r"/app/assets/([^\s\"']+)", index.read_text(encoding="utf-8")):
|
||||
name = m.group(1).split("/")[-1]
|
||||
if name in all_names:
|
||||
keep_names.add(name)
|
||||
|
||||
if not keep_names:
|
||||
raise SystemExit("no entry assets found in index.html")
|
||||
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for name in list(keep_names):
|
||||
path = assets / name
|
||||
if not path.is_file():
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
for ref in _referenced_basenames(text, all_names):
|
||||
if ref not in keep_names:
|
||||
keep_names.add(ref)
|
||||
changed = True
|
||||
|
||||
return {assets / n for n in keep_names}
|
||||
|
||||
|
||||
def _partition_orphans(
|
||||
orphan: list[Path],
|
||||
retention_days: int,
|
||||
) -> tuple[list[Path], list[Path]]:
|
||||
"""Split orphans into delete-now vs keep-for-open-tabs (mtime within window)."""
|
||||
if retention_days <= 0:
|
||||
return orphan, []
|
||||
cutoff = time.time() - retention_days * 86400
|
||||
to_remove: list[Path] = []
|
||||
retained: list[Path] = []
|
||||
for p in orphan:
|
||||
if p.stat().st_mtime >= cutoff:
|
||||
retained.append(p)
|
||||
else:
|
||||
to_remove.append(p)
|
||||
return to_remove, retained
|
||||
|
||||
|
||||
def main() -> int:
|
||||
dry_run, retention_days, app_dir = _parse_args(sys.argv[1:])
|
||||
assets = app_dir / "assets"
|
||||
if not assets.is_dir():
|
||||
raise SystemExit(f"not a directory: {assets}")
|
||||
|
||||
keep = collect(app_dir)
|
||||
all_files = {p for p in assets.iterdir() if p.is_file()}
|
||||
orphan = sorted(all_files - keep, key=lambda p: p.name)
|
||||
to_remove, retained = _partition_orphans(orphan, retention_days)
|
||||
|
||||
if dry_run:
|
||||
print(
|
||||
f"dry-run retention_days={retention_days} "
|
||||
f"referenced_kept={len(keep)} "
|
||||
f"would_remove={len(to_remove)} "
|
||||
f"retained_orphans={len(retained)} "
|
||||
f"total={len(all_files)}"
|
||||
)
|
||||
return 0
|
||||
|
||||
for p in to_remove:
|
||||
p.unlink()
|
||||
print(
|
||||
f"kept={len(keep)} removed={len(to_remove)} "
|
||||
f"retained_orphans={len(retained)} retention_days={retention_days} "
|
||||
f"total_before={len(all_files)}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Nexus 6.0 — 公共仓库一键安装入口(参考 1Panel quick_start.sh)
|
||||
#
|
||||
# root 登录后一条命令:
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" | bash
|
||||
#
|
||||
# 或保存后执行:
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/quick-install.sh" -o /tmp/quick-install.sh
|
||||
# bash /tmp/quick-install.sh
|
||||
#
|
||||
# 可选参数会传给 install-nexus-fresh.sh,例如:
|
||||
# bash /tmp/quick-install.sh --profile 4c16g
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
|
||||
NEXUS_GITEA_SCHEME="${NEXUS_GITEA_SCHEME:-https}"
|
||||
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
|
||||
NEXUS_GIT_BRANCH="${NEXUS_GIT_BRANCH:-main}"
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_RAW_BASE="${NEXUS_RAW_BASE:-${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/branch/${NEXUS_GIT_BRANCH}}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Nexus 公共仓库一键安装
|
||||
|
||||
curl -fsSL "${NEXUS_RAW_BASE}/deploy/quick-install.sh" | bash
|
||||
|
||||
可选参数: --profile 4c16g --domain 你的域名 --skip-1panel-check
|
||||
EOF
|
||||
}
|
||||
|
||||
validate_script_file() {
|
||||
local f="$1"
|
||||
if [[ ! -f "$f" ]] || ! head -1 "$f" | grep -q '^#!/'; then
|
||||
return 1
|
||||
fi
|
||||
if grep -qiE '^(Not found|404)' "$f" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
download_script() {
|
||||
local name="$1" dest="$2"
|
||||
local url base
|
||||
for base in \
|
||||
"${NEXUS_RAW_BASE}" \
|
||||
"${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/admin/Nexus/raw/${NEXUS_GIT_BRANCH}"; do
|
||||
url="${base}/deploy/${name}"
|
||||
if curl -fsSL "$url" -o "$dest" 2>/dev/null && validate_script_file "$dest"; then
|
||||
info "已下载 ${name}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
run_via_git() {
|
||||
local url="${NEXUS_GITEA_SCHEME}://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
|
||||
url="${NEXUS_GITEA_SCHEME}://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
|
||||
fi
|
||||
step "改用 git clone..."
|
||||
mkdir -p "$(dirname "$NEXUS_ROOT")"
|
||||
if [[ -d "${NEXUS_ROOT}/.git" ]]; then
|
||||
git -C "$NEXUS_ROOT" remote set-url origin "$url" 2>/dev/null || true
|
||||
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --depth 1 2>/dev/null || \
|
||||
git -C "$NEXUS_ROOT" fetch origin "$NEXUS_GIT_BRANCH" --prune
|
||||
git -C "$NEXUS_ROOT" reset --hard "origin/${NEXUS_GIT_BRANCH}" 2>/dev/null || \
|
||||
git -C "$NEXUS_ROOT" pull
|
||||
else
|
||||
rm -rf "$NEXUS_ROOT"
|
||||
git clone --depth 1 -b "$NEXUS_GIT_BRANCH" "$url" "$NEXUS_ROOT"
|
||||
fi
|
||||
chmod +x "${NEXUS_ROOT}/deploy/"*.sh 2>/dev/null || true
|
||||
exec bash "${NEXUS_ROOT}/deploy/install-nexus-fresh.sh" --skip-clone "$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行(su - 后直接运行,不要用 sudo 包一层)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cf in /root/.nexus-deploy.env /root/.nexus-secrets/nexus.env; do
|
||||
[[ -f "$cf" ]] && set -a && source "$cf" && set +a
|
||||
done
|
||||
unset NEXUS_SECRET_KEY NEXUS_API_KEY NEXUS_ENCRYPTION_KEY
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} Nexus 6.0 — 公共仓库一键安装 ${BLUE}║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
esac
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp /tmp/install-nexus-fresh.XXXXXX.sh)"
|
||||
if download_script "install-nexus-fresh.sh" "$tmp"; then
|
||||
chmod +x "$tmp"
|
||||
exec bash "$tmp" "$@"
|
||||
fi
|
||||
|
||||
step "raw 下载失败,尝试 git clone..."
|
||||
run_via_git "$@"
|
||||
}
|
||||
|
||||
# curl | bash 时 stdin 脚本无 BASH_SOURCE[0],不可配合 set -u 做入口判断
|
||||
main "$@"
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync local Nexus working tree to production host (no Gitea push/pull).
|
||||
# Transfer via tar|ssh; remote ownership auto-detected (www:www on BT, else deploy path owner).
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/rsync-local-to-server.sh
|
||||
# NEXUS_DEPLOY_PATH=/opt/nexus bash deploy/rsync-local-to-server.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT}"
|
||||
|
||||
SECRETS="${ROOT}/deploy/nexus-1panel.secrets.sh"
|
||||
if [[ -f "$SECRETS" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$SECRETS"
|
||||
fi
|
||||
|
||||
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
|
||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
|
||||
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
|
||||
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
|
||||
fi
|
||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||
|
||||
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
||||
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-}"
|
||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
||||
DEPLOY_PATH="$(ssh_cmd 'for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do [[ -d "$d/server" ]] && echo "$d" && exit 0; done; echo /opt/nexus')"
|
||||
fi
|
||||
|
||||
resolve_deploy_chown() {
|
||||
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
|
||||
echo "${NEXUS_DEPLOY_CHOWN}"
|
||||
return
|
||||
fi
|
||||
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
|
||||
echo "www:www"
|
||||
return
|
||||
fi
|
||||
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
|
||||
}
|
||||
DEPLOY_CHOWN="$(resolve_deploy_chown)"
|
||||
|
||||
echo "═══ Nexus local → server sync ═══"
|
||||
echo "SSH target: ${NEXUS_SSH_HOST}"
|
||||
echo "Remote path: ${DEPLOY_PATH}"
|
||||
echo "Owner: ${DEPLOY_CHOWN}"
|
||||
|
||||
TAR_EXCLUDES=(
|
||||
--exclude='.git'
|
||||
--exclude='.env'
|
||||
--exclude='.env.*'
|
||||
--exclude='SECRETS.md'
|
||||
--exclude='deploy/nexus-1panel.secrets.sh'
|
||||
--exclude='docker/.env.prod'
|
||||
--exclude='web/data'
|
||||
--exclude='frontend/node_modules'
|
||||
--exclude='node_modules'
|
||||
--exclude='.venv'
|
||||
--exclude='venv'
|
||||
--exclude='__pycache__'
|
||||
--exclude='.cursor'
|
||||
--exclude='.install_locked'
|
||||
--exclude='.install_state.json'
|
||||
--exclude='web/app/assets'
|
||||
--exclude='tmp'
|
||||
--exclude='frontend/test-results'
|
||||
--exclude='frontend/e2e/.auth'
|
||||
)
|
||||
|
||||
echo "▶ tar stream → ${DEPLOY_PATH} ..."
|
||||
tar -C "${ROOT}" -czf - "${TAR_EXCLUDES[@]}" . | \
|
||||
ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "sudo tar -xzf - -C '${DEPLOY_PATH}'"
|
||||
|
||||
echo "▶ chown ${DEPLOY_CHOWN} (skip docker/.env.prod, web/data, .env) ..."
|
||||
ssh_cmd "sudo bash -s" <<REMOTE
|
||||
set -euo pipefail
|
||||
root='${DEPLOY_PATH}'
|
||||
own='${DEPLOY_CHOWN}'
|
||||
chown_tree() {
|
||||
local p="\$1"
|
||||
[[ -e "\$p" ]] || return 0
|
||||
sudo chown -R "\$own" "\$p"
|
||||
}
|
||||
for top in server deploy scripts tests standards docs frontend web; do
|
||||
chown_tree "\$root/\$top"
|
||||
done
|
||||
if [[ -d "\$root/docker" ]]; then
|
||||
while IFS= read -r -d '' p; do
|
||||
sudo chown -R "\$own" "\$p"
|
||||
done < <(find "\$root/docker" \( -path "\$root/docker/.env.prod" \) -prune -o -print0)
|
||||
fi
|
||||
for f in pyproject.toml requirements.txt requirements-dev.txt AGENTS.md CLAUDE.md; do
|
||||
[[ -f "\$root/\$f" ]] && sudo chown "\$own" "\$root/\$f"
|
||||
done
|
||||
REMOTE
|
||||
|
||||
echo "✓ sync complete (owner ${DEPLOY_CHOWN})"
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Run tests/test_api.py on the deploy host (reads NEXUS_TEST_* from .env).
|
||||
set -euo pipefail
|
||||
cd /www/wwwroot/api.synaglobal.vip
|
||||
export NEXUS_TEST_BASE="${NEXUS_TEST_BASE:-http://127.0.0.1:8600}"
|
||||
python3 tests/test_api.py
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync install wizard static files into the running Nexus prod container.
|
||||
# Primary fix: Dockerfile.prod bakes these into the image on build.
|
||||
# This script is a safety net after upgrade if an old image is still running.
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
CONTAINER="${NEXUS_CONTAINER:-}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
resolve_container() {
|
||||
if [[ -n "$CONTAINER" ]]; then
|
||||
return 0
|
||||
fi
|
||||
CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$CONTAINER" ]]; then
|
||||
error "未找到运行中的 Nexus 容器(可设 NEXUS_CONTAINER=名称)"
|
||||
exit 1
|
||||
fi
|
||||
info "容器: $CONTAINER"
|
||||
}
|
||||
|
||||
sync_file() {
|
||||
local src="$1" dest="$2"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
error "缺少源文件: $src"
|
||||
exit 1
|
||||
fi
|
||||
docker exec "$CONTAINER" mkdir -p "$(dirname "$dest")"
|
||||
docker cp "$src" "${CONTAINER}:${dest}"
|
||||
}
|
||||
|
||||
wizard_archived_in_container() {
|
||||
docker exec "$CONTAINER" test -f /app/.install_locked 2>/dev/null \
|
||||
|| docker exec "$CONTAINER" test -f /var/lib/nexus/.install_locked 2>/dev/null \
|
||||
|| docker exec "$CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null
|
||||
}
|
||||
|
||||
main() {
|
||||
local app="$NEXUS_ROOT/web/app"
|
||||
local wizard_archived=false
|
||||
if [[ ! -f "$app/install.html" ]]; then
|
||||
error "仓库中无 $app/install.html,请先 git pull"
|
||||
exit 1
|
||||
fi
|
||||
resolve_container
|
||||
if wizard_archived_in_container; then
|
||||
wizard_archived=true
|
||||
info "安装已锁定或向导已归档,跳过静态/Python 热同步"
|
||||
local code
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "404" ]]; then
|
||||
info "/app/install.html 已归档 → HTTP 404"
|
||||
exit 0
|
||||
fi
|
||||
warn "/app/install.html 应已归档但返回 HTTP $code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "同步安装向导静态文件..."
|
||||
sync_file "$app/install.html" /app/web/app/install.html
|
||||
sync_file "$app/vendor/tailwindcss-browser.js" /app/web/app/vendor/tailwindcss-browser.js
|
||||
sync_file "$app/vendor/theme-init.js" /app/web/app/vendor/theme-init.js
|
||||
sync_file "$app/vendor/theme.css" /app/web/app/vendor/theme.css
|
||||
sync_file "$app/vendor/alpinejs.min.js" /app/web/app/vendor/alpinejs.min.js
|
||||
|
||||
local install_py="$NEXUS_ROOT/server/api/install.py" code
|
||||
if [[ -f "$install_py" ]]; then
|
||||
info "同步 install.py(Docker Redis env-check 修复)..."
|
||||
sync_file "$install_py" /app/server/api/install.py
|
||||
info "重启 Nexus 容器以加载 Python 变更..."
|
||||
docker restart "$CONTAINER" >/dev/null
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/health" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
else
|
||||
warn "未找到 $install_py,跳过 Python 热同步"
|
||||
fi
|
||||
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "200" ]]; then
|
||||
info "/app/install.html → HTTP 200"
|
||||
else
|
||||
warn "/app/install.html → HTTP $code(请检查 Nexus 是否在 ${NEXUS_PUBLISH_PORT} 监听)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync Git-tracked web/app and web/app-v2 into the running Nexus Docker container.
|
||||
#
|
||||
# Root cause: Dockerfile.prod runs vite build inside the image; Docker layer cache
|
||||
# can produce asset hashes that differ from committed web/app on the host.
|
||||
#
|
||||
# Usage (on production host after git pull / upgrade):
|
||||
# NEXUS_ROOT=/opt/nexus bash deploy/sync_webapp_to_container.sh
|
||||
#
|
||||
# Called automatically from deploy/nexus-1panel.sh cmd_upgrade and deploy-production.sh.
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
CONTAINER="${NEXUS_CONTAINER:-}"
|
||||
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
||||
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then return 0; fi
|
||||
sudo docker "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
resolve_container() {
|
||||
if [[ -n "$CONTAINER" ]]; then
|
||||
return 0
|
||||
fi
|
||||
CONTAINER="$(docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$CONTAINER" ]]; then
|
||||
error "未找到运行中的 Nexus 容器(可设 NEXUS_CONTAINER=名称)"
|
||||
exit 1
|
||||
fi
|
||||
info "容器: $CONTAINER"
|
||||
}
|
||||
|
||||
verify_entry_bundle() {
|
||||
local prefix html bundle_path code
|
||||
prefix="$1"
|
||||
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${prefix}/" 2>/dev/null || true)"
|
||||
bundle_path="$(printf '%s' "$html" | grep -oE "${prefix}"'/assets/index-[^"]+\.js' | head -1 || true)"
|
||||
if [[ -z "$bundle_path" ]]; then
|
||||
error "${prefix}/ index.html 未解析到主 bundle"
|
||||
exit 1
|
||||
fi
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${bundle_path}" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" != "200" ]]; then
|
||||
error "主 bundle ${bundle_path} → HTTP ${code}"
|
||||
exit 1
|
||||
fi
|
||||
info "入口校验 OK: ${bundle_path} → HTTP 200"
|
||||
}
|
||||
|
||||
main() {
|
||||
local app="${NEXUS_ROOT}/web/app"
|
||||
local app_v2="${NEXUS_ROOT}/web/app-v2"
|
||||
if [[ ! -f "${app}/index.html" ]]; then
|
||||
error "缺少 ${app}/index.html,请先 git pull"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${app}/assets" ]]; then
|
||||
error "缺少 ${app}/assets"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "${app_v2}/index.html" ]]; then
|
||||
error "缺少 ${app_v2}/index.html,请先构建 frontend-v2"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${app_v2}/assets" ]]; then
|
||||
error "缺少 ${app_v2}/assets"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
resolve_container
|
||||
|
||||
info "同步 web/app → 容器 /app/web/app ..."
|
||||
docker_cmd exec "$CONTAINER" rm -rf /app/web/app/assets
|
||||
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app
|
||||
docker_cmd cp "${app}/index.html" "${CONTAINER}:/app/web/app/index.html"
|
||||
docker_cmd cp "${app}/assets" "${CONTAINER}:/app/web/app/assets"
|
||||
|
||||
if [[ -f "${app}/favicon.ico" ]]; then
|
||||
docker_cmd cp "${app}/favicon.ico" "${CONTAINER}:/app/web/app/favicon.ico" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
info "同步 web/app-v2 → 容器 /app/web/app-v2 ..."
|
||||
docker_cmd exec "$CONTAINER" rm -rf /app/web/app-v2/assets
|
||||
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app-v2
|
||||
docker_cmd cp "${app_v2}/index.html" "${CONTAINER}:/app/web/app-v2/index.html"
|
||||
docker_cmd cp "${app_v2}/assets" "${CONTAINER}:/app/web/app-v2/assets"
|
||||
|
||||
if [[ -x "${NEXUS_ROOT}/deploy/prune_frontend_assets.py" ]]; then
|
||||
info "容器内 prune 未引用 assets(可选)..."
|
||||
docker_cmd exec "$CONTAINER" python3 /app/deploy/prune_frontend_assets.py --dry-run 2>/dev/null || \
|
||||
warn "容器内 prune 跳过(脚本或路径不可用)"
|
||||
fi
|
||||
|
||||
verify_entry_bundle "/app"
|
||||
verify_entry_bundle "/app-v2"
|
||||
info "web/app 与 web/app-v2 同步完成"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Diagnose 1Panel Redis auth from Nexus container network perspective.
|
||||
# Usage:
|
||||
# REDIS_PASS='redis_WP3NyN' bash deploy/test-1panel-redis.sh
|
||||
# bash deploy/test-1panel-redis.sh # prompts for password
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
ENV_PROD="${NEXUS_ROOT}/docker/.env.prod"
|
||||
|
||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
||||
ok() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
||||
fail() { echo -e "\033[0;31m[FAIL]\033[0m $*"; }
|
||||
|
||||
pick_redis_container() {
|
||||
if [[ -n "${REDIS_CONTAINER:-}" ]]; then
|
||||
echo "$REDIS_CONTAINER"
|
||||
return 0
|
||||
fi
|
||||
local h
|
||||
h="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$h" ]]; then
|
||||
echo "$h"
|
||||
return 0
|
||||
fi
|
||||
if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then
|
||||
h="$("$NEXUS_ROOT/deploy/detect-1panel-services.sh" 2>/dev/null | sed -n 's/^NEXUS_1PANEL_REDIS_HOST=//p' | head -1)"
|
||||
[[ -n "$h" ]] && echo "$h" && return 0
|
||||
fi
|
||||
docker ps --format '{{.Names}}' | grep -iE '^1Panel-redis-' | head -1 || true
|
||||
}
|
||||
|
||||
try_ping() {
|
||||
local label="$1" url="$2"
|
||||
local nexus
|
||||
nexus="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$nexus" ]]; then
|
||||
fail "未找到 Nexus 容器"
|
||||
return 1
|
||||
fi
|
||||
if docker exec "$nexus" python3 -c "
|
||||
import redis
|
||||
r = redis.Redis.from_url('${url}', socket_timeout=3)
|
||||
print(r.ping())
|
||||
" 2>/dev/null | grep -q True; then
|
||||
ok "$label → $url"
|
||||
return 0
|
||||
fi
|
||||
fail "$label"
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
local redis_host pass
|
||||
redis_host="$(pick_redis_container)"
|
||||
[[ -n "$redis_host" ]] || { fail "未找到 Redis 容器/主机名"; exit 1; }
|
||||
info "Redis 主机: $redis_host"
|
||||
|
||||
if [[ -z "${REDIS_PASS:-}" ]]; then
|
||||
read -r -s -p "1Panel Redis 密码(应用参数): " pass
|
||||
echo ""
|
||||
else
|
||||
pass="$REDIS_PASS"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "从 Nexus 容器内尝试多种 URL 格式(807f4c2 修复后)..."
|
||||
local any=0
|
||||
try_ping "无认证" "redis://${redis_host}:6379/0" && any=1 || true
|
||||
if [[ -n "$pass" ]]; then
|
||||
try_ping "仅密码 (:pass@)" "redis://:${pass}@${redis_host}:6379/0" && any=1 || true
|
||||
try_ping "default 用户" "redis://default:${pass}@${redis_host}:6379/0" && any=1 || true
|
||||
info "跳过 root 用户探测 — 1Panel Redis 无 root 账号"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$any" -eq 1 ]]; then
|
||||
ok "至少一种格式可用"
|
||||
echo ""
|
||||
info "安装向导步骤 3:1Panel Redis 无账号,只填密码(后端自动用 :pass@ 或 default)"
|
||||
info "已有 .env 时写入 NEXUS_REDIS_URL,例如:"
|
||||
info " redis://:${pass}@${redis_host}:6379/0"
|
||||
info " 或 redis://default:${pass}@${redis_host}:6379/0"
|
||||
exit 0
|
||||
fi
|
||||
fail "全部失败。请在 1Panel → Redis → 参数 核对密码,或检查 Nexus 是否在 1panel-network"
|
||||
info "本机: docker exec -it ${redis_host} redis-cli -u 'redis://:密码@127.0.0.1:6379' ping"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 卸载 Nexus 旧版 Compose 内置 MySQL 容器
|
||||
#
|
||||
# bash deploy/uninstall-mysql-compose.sh
|
||||
# bash deploy/uninstall-mysql-compose.sh --purge-volume
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
|
||||
PURGE_VOLUME=false
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
卸载 Nexus Compose 遗留 MySQL 容器
|
||||
|
||||
bash deploy/uninstall-mysql-compose.sh [--purge-volume]
|
||||
|
||||
说明:
|
||||
- 不卸载宿主机/1Panel 自建的 MySQL
|
||||
- 卸载后请自建库 nexus + 用户,安装向导步骤 3 填写连接信息
|
||||
EOF
|
||||
}
|
||||
|
||||
compose_env_args() {
|
||||
local_env=()
|
||||
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
|
||||
local prof="${NEXUS_ROOT}/docker/.host-profile"
|
||||
if [[ -f "$prof" ]]; then
|
||||
local p
|
||||
p="$(tr -d '\r\n' <"$prof")"
|
||||
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
|
||||
fi
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--purge-volume) PURGE_VOLUME=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ "$(id -u)" -eq 0 ]] || { error "请使用 root 执行"; exit 1; }
|
||||
|
||||
removed=0
|
||||
|
||||
stop_rm_container() {
|
||||
local name="$1"
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
|
||||
info "停止并删除容器: $name"
|
||||
docker stop "$name" >/dev/null 2>&1 || true
|
||||
docker rm -f "$name" >/dev/null 2>&1 || true
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
for pattern in nexus-prod-mysql-1 nexus-prod-mysql nexus-mysql-1; do
|
||||
stop_rm_container "$pattern"
|
||||
done
|
||||
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
|
||||
compose_env_args
|
||||
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]] && grep -q '^ mysql:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
|
||||
warn "compose 仍含 mysql 服务,尝试 compose rm..."
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop mysql 2>/dev/null || true)
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f mysql 2>/dev/null || true)
|
||||
removed=$((removed + 1))
|
||||
elif [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" && -f "${NEXUS_ROOT}/docker/.env.prod" ]]; then
|
||||
info "Compose --remove-orphans(从 nexus-prod 项目移除 mysql 孤儿容器)..."
|
||||
compose_files=(-f "${NEXUS_ROOT}/${COMPOSE_FILE}")
|
||||
if docker network inspect 1panel-network >/dev/null 2>&1 \
|
||||
&& [[ -f "${NEXUS_ROOT}/docker/docker-compose.1panel.yml" ]]; then
|
||||
compose_files+=(-f "${NEXUS_ROOT}/docker/docker-compose.1panel.yml")
|
||||
fi
|
||||
(cd "$NEXUS_ROOT" && docker compose "${compose_files[@]}" "${local_env[@]}" up -d --remove-orphans 2>/dev/null || true)
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
fi
|
||||
|
||||
if [[ "$PURGE_VOLUME" == true ]]; then
|
||||
while IFS= read -r vol; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
info "删除数据卷: $vol"
|
||||
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol"
|
||||
done < <(docker volume ls -q | grep -iE 'nexus.*mysql|mysql.*nexus' || true)
|
||||
fi
|
||||
|
||||
leftover="$(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*mysql|mysql.*nexus' || true)"
|
||||
if [[ -n "$leftover" ]]; then
|
||||
warn "仍有 MySQL 相关容器:"
|
||||
echo "$leftover" | sed 's/^/ /'
|
||||
warn "若确认为 Compose 遗留,可: docker rm -f <容器名>"
|
||||
elif [[ "$removed" -gt 0 ]]; then
|
||||
info "Compose MySQL 容器已卸载。"
|
||||
else
|
||||
info "未发现 Nexus Compose MySQL 容器。"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "当前 Nexus 栈:"
|
||||
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | grep -i nexus || docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
|
||||
echo ""
|
||||
info "下一步:1Panel 应用商店安装 MySQL → nx update 探测容器名 → 向导步骤 3 使用 1Panel-mysql-xxxx"
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 卸载 Nexus 旧版 Compose 内置 Redis 容器
|
||||
#
|
||||
# 适用:已从 docker-compose.prod.yml 移除 redis 服务,改用宿主机/1Panel 自建 Redis。
|
||||
# 不会卸载 apt/1Panel 安装的 Redis,仅删除 Docker 容器与(可选)数据卷。
|
||||
#
|
||||
# bash deploy/uninstall-redis-compose.sh
|
||||
# bash deploy/uninstall-redis-compose.sh --purge-volume # 同时删 redis-data 卷
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker/docker-compose.prod.yml}"
|
||||
PURGE_VOLUME=false
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
卸载 Nexus Compose 遗留 Redis 容器
|
||||
|
||||
bash deploy/uninstall-redis-compose.sh [--purge-volume]
|
||||
|
||||
选项:
|
||||
--purge-volume 删除 nexus-prod_redis-data 数据卷(不可恢复)
|
||||
-h, --help 显示帮助
|
||||
|
||||
说明:
|
||||
- 不卸载宿主机 redis-server / 1Panel 应用商店 Redis
|
||||
- 卸载后请在宿主机安装 Redis,安装向导步骤 2 须能 ping 通
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--purge-volume) PURGE_VOLUME=true; shift ;;
|
||||
*) error "未知参数: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
error "请使用 root 执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
removed=0
|
||||
|
||||
stop_rm_container() {
|
||||
local name="$1"
|
||||
if docker ps -a --format '{{.Names}}' | grep -qx "$name"; then
|
||||
info "停止并删除容器: $name"
|
||||
docker stop "$name" >/dev/null 2>&1 || true
|
||||
docker rm "$name" >/dev/null 2>&1 || true
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# 常见 Compose 命名
|
||||
for pattern in nexus-prod-redis-1 nexus-prod-redis nexus-redis-1; do
|
||||
stop_rm_container "$pattern"
|
||||
done
|
||||
|
||||
# 名称含 nexus 且含 redis 的容器
|
||||
while IFS= read -r c; do
|
||||
[[ -z "$c" ]] && continue
|
||||
stop_rm_container "$c"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -iE 'nexus.*redis|redis.*nexus' || true)
|
||||
|
||||
# 若旧 compose 仍定义 redis 服务
|
||||
if [[ -f "${NEXUS_ROOT}/${COMPOSE_FILE}" ]]; then
|
||||
if grep -q '^ redis:' "${NEXUS_ROOT}/${COMPOSE_FILE}" 2>/dev/null; then
|
||||
warn "compose 仍含 redis 服务,尝试 compose rm..."
|
||||
local_env=()
|
||||
[[ -f "${NEXUS_ROOT}/docker/.env.prod" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/.env.prod")
|
||||
prof="${NEXUS_ROOT}/docker/.host-profile"
|
||||
if [[ -f "$prof" ]]; then
|
||||
p="$(tr -d '\r\n' <"$prof")"
|
||||
[[ -f "${NEXUS_ROOT}/docker/profiles/${p}.env" ]] && local_env+=(--env-file "${NEXUS_ROOT}/docker/profiles/${p}.env")
|
||||
fi
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" stop redis 2>/dev/null || true)
|
||||
(cd "$NEXUS_ROOT" && docker compose -f "$COMPOSE_FILE" "${local_env[@]}" rm -f redis 2>/dev/null || true)
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$PURGE_VOLUME" == true ]]; then
|
||||
while IFS= read -r vol; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
info "删除数据卷: $vol"
|
||||
docker volume rm "$vol" 2>/dev/null || warn "无法删除卷 $vol(可能被占用)"
|
||||
done < <(docker volume ls -q | grep -iE 'nexus.*redis|redis.*nexus' || true)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$removed" -gt 0 ]]; then
|
||||
info "Compose Redis 容器已卸载。"
|
||||
else
|
||||
info "未发现 Nexus Compose Redis 容器(可能已删除)。"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "下一步:1Panel 应用商店安装 Redis → nx update 写入 NEXUS_1PANEL_REDIS_HOST"
|
||||
echo " 向导 Redis URL:redis://:密码@1Panel-redis-xxxx:6379/0(无 root 用户)"
|
||||
echo " 诊断:bash deploy/test-1panel-redis.sh"
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
# ================================================================
|
||||
# Nexus 6.0 — Uninstall Script
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash uninstall.sh # interactive
|
||||
# sudo bash uninstall.sh --deploy-dir /opt/nexus # specify deploy dir
|
||||
# sudo bash uninstall.sh --purge # also remove deploy dir
|
||||
#
|
||||
# Safe: stops services, removes configs. Database is NEVER deleted automatically.
|
||||
# ================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
DEPLOY_DIR=""
|
||||
PURGE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
|
||||
--purge) PURGE=true; shift ;;
|
||||
-h|--help)
|
||||
echo "Usage: sudo bash uninstall.sh [--deploy-dir PATH] [--purge]"
|
||||
echo ""
|
||||
echo " --deploy-dir PATH Specify Nexus installation directory"
|
||||
echo " --purge Also remove the deploy directory and venv"
|
||||
echo ""
|
||||
echo " Note: MySQL database is NEVER deleted automatically."
|
||||
exit 0 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Auto-detect deploy directory
|
||||
if [ -z "$DEPLOY_DIR" ]; then
|
||||
for d in /opt/nexus /www/wwwroot/*/; do
|
||||
if [ -d "$d/server" ] && [ -d "$d/web" ]; then
|
||||
DEPLOY_DIR="$d"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$DEPLOY_DIR" ] || [ ! -d "$DEPLOY_DIR/server" ]; then
|
||||
error "Cannot find Nexus installation. Use --deploy-dir to specify."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect BT Panel
|
||||
BT_MODE=false
|
||||
if [ -d "/www/server/panel" ] && [ -f "/www/server/panel/class/common.py" ]; then
|
||||
BT_MODE=true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Nexus 6.0 Uninstall"
|
||||
echo "=========================================="
|
||||
echo " Deploy: $DEPLOY_DIR"
|
||||
echo " BT Panel: $BT_MODE"
|
||||
echo " Purge files: $PURGE"
|
||||
echo ""
|
||||
|
||||
# Confirm
|
||||
read -rp "Are you sure you want to uninstall Nexus? [y/N] " confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||
echo "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. Stop Supervisor process
|
||||
info "Step 1/5: Stopping Nexus process..."
|
||||
supervisorctl stop nexus >/dev/null 2>&1 || true
|
||||
|
||||
# 2. Remove Supervisor config
|
||||
info "Step 2/5: Removing Supervisor config..."
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
SUPERVISOR_CONF="/www/server/panel/plugin/supervisor/profile/nexus.ini"
|
||||
else
|
||||
SUPERVISOR_CONF="/etc/supervisor/conf.d/nexus.conf"
|
||||
fi
|
||||
if [ -f "$SUPERVISOR_CONF" ]; then
|
||||
rm -f "$SUPERVISOR_CONF"
|
||||
supervisorctl reread >/dev/null 2>&1 || true
|
||||
supervisorctl update >/dev/null 2>&1 || true
|
||||
info " Removed $SUPERVISOR_CONF"
|
||||
else
|
||||
info " Supervisor config not found (already removed)"
|
||||
fi
|
||||
|
||||
# 3. Remove Nginx config
|
||||
info "Step 3/5: Removing Nginx config..."
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
# Find the domain from deploy dir
|
||||
DOMAIN=$(basename "$DEPLOY_DIR")
|
||||
NGINX_CONF="/www/server/panel/vhost/nginx/$DOMAIN.conf"
|
||||
else
|
||||
NGINX_CONF="/etc/nginx/sites-available/nexus"
|
||||
NGINX_SYMLINK="/etc/nginx/sites-enabled/nexus"
|
||||
fi
|
||||
|
||||
if [ -f "$NGINX_CONF" ]; then
|
||||
rm -f "$NGINX_CONF"
|
||||
[ -L "${NGINX_SYMLINK:-}" ] && rm -f "$NGINX_SYMLINK"
|
||||
if [ "$BT_MODE" = true ]; then
|
||||
/www/server/nginx/sbin/nginx -s reload >/dev/null 2>&1 || true
|
||||
else
|
||||
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
|
||||
fi
|
||||
info " Removed $NGINX_CONF"
|
||||
else
|
||||
info " Nginx config not found (already removed)"
|
||||
fi
|
||||
|
||||
# 4. Remove crontab entries
|
||||
info "Step 4/5: Removing crontab entries..."
|
||||
crontab -l 2>/dev/null | grep -v "health_monitor.sh" | grep -v "db_backup.sh" | crontab - 2>/dev/null || true
|
||||
info " Crontab entries removed"
|
||||
|
||||
# 5. Optionally remove deploy directory
|
||||
if [ "$PURGE" = true ]; then
|
||||
info "Step 5/5: Removing deploy directory (--purge)..."
|
||||
rm -rf "$DEPLOY_DIR"
|
||||
info " Removed $DEPLOY_DIR"
|
||||
else
|
||||
info "Step 5/5: Keeping deploy directory (use --purge to remove)"
|
||||
info " To remove manually: rm -rf $DEPLOY_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${GREEN}Nexus uninstalled successfully.${NC}"
|
||||
echo ""
|
||||
echo " Note: MySQL database was NOT deleted."
|
||||
echo " To drop the database manually:"
|
||||
echo " mysql -u root -e 'DROP DATABASE IF EXISTS nexus;'"
|
||||
echo ""
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus 6.0 — 一键更新(拉代码 + 重建镜像 + 重启)
|
||||
#
|
||||
# 已在 /opt/nexus:
|
||||
# bash deploy/update.sh
|
||||
# bash deploy/update.sh --no-cache # 强制无缓存重建(entrypoint/Dockerfile 变更后)
|
||||
#
|
||||
# 远程一条命令(推荐在仓库目录执行,管道模式会回退 /opt/nexus):
|
||||
# cd /opt/nexus && bash deploy/update.sh
|
||||
# curl -fsSL "https://axs.kuma1xn.vip/admin/Nexus/raw/branch/main/deploy/update.sh" | bash
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
_resolve_nexus_root() {
|
||||
local src="${BASH_SOURCE[0]:-}"
|
||||
if [[ -n "$src" && "$src" != /dev/fd/* && "$src" != "-" && -f "$src" ]]; then
|
||||
cd "$(dirname "$(readlink -f "$src")")/.." && pwd
|
||||
return
|
||||
fi
|
||||
if [[ -d /opt/nexus ]]; then
|
||||
echo "警告: 通过管道执行无法解析脚本路径,已回退 /opt/nexus;推荐: cd /opt/nexus && bash deploy/update.sh" >&2
|
||||
echo /opt/nexus
|
||||
return
|
||||
fi
|
||||
echo "错误: 无法定位 Nexus 根目录;请 cd /opt/nexus && bash deploy/update.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ROOT="$(_resolve_nexus_root)"
|
||||
|
||||
NEXUS_ROOT="$ROOT" bash "$ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||
exec bash "$ROOT/deploy/nexus-1panel.sh" upgrade "$@"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
# 兼容入口 — 请优先使用 deploy/nexus-1panel.sh
|
||||
exec "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/nexus-1panel.sh" upgrade "$@"
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# Nexus 6.0 — Upgrade Script(自动识别 Docker / Supervisor)
|
||||
#
|
||||
# sudo bash deploy/upgrade.sh
|
||||
# sudo bash deploy/upgrade.sh --deploy-dir /opt/nexus
|
||||
#
|
||||
# Docker 1Panel:委托 deploy/nexus-1panel.sh upgrade(拉代码 + 备份 + 重建镜像)
|
||||
# Supervisor 裸机:git pull + venv pip + supervisorctl restart
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=./detect_deploy_runtime.sh
|
||||
source "${SCRIPT_DIR}/detect_deploy_runtime.sh"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
DEPLOY_DIR=""
|
||||
EXTRA_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--deploy-dir) DEPLOY_DIR="$2"; shift 2 ;;
|
||||
--no-backup|--require-backup|--no-cache|--prune|--check|--dry-run)
|
||||
EXTRA_ARGS+=("$1"); shift ;;
|
||||
-h|--help)
|
||||
cat <<'EOF'
|
||||
用法: sudo bash deploy/upgrade.sh [--deploy-dir PATH] [nexus-1panel 升级选项]
|
||||
|
||||
Docker 环境自动走 nexus-1panel.sh upgrade;Supervisor 环境走 venv + supervisorctl。
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*) EXTRA_ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$DEPLOY_DIR" ]]; then
|
||||
DEPLOY_DIR="$(resolve_nexus_root 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "$DEPLOY_DIR" || ! -d "$DEPLOY_DIR/server" ]]; then
|
||||
error "找不到 Nexus 安装目录,请使用 --deploy-dir 指定"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUNTIME="$(detect_nexus_runtime "$DEPLOY_DIR")"
|
||||
|
||||
if [[ "$RUNTIME" == "docker" ]]; then
|
||||
info "检测到 Docker Compose 运行时,委托 nexus-1panel.sh upgrade"
|
||||
exec env NEXUS_ROOT="$DEPLOY_DIR" bash "$DEPLOY_DIR/deploy/nexus-1panel.sh" upgrade "${EXTRA_ARGS[@]}"
|
||||
fi
|
||||
|
||||
VENV_DIR="$DEPLOY_DIR/venv"
|
||||
ENV_FILE="$DEPLOY_DIR/.env"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Nexus 6.0 Upgrade (Supervisor)"
|
||||
echo "=========================================="
|
||||
echo " Deploy: $DEPLOY_DIR"
|
||||
echo ""
|
||||
|
||||
info "Step 1/4: Updating source code..."
|
||||
if [[ -d "$DEPLOY_DIR/.git" ]]; then
|
||||
cd "$DEPLOY_DIR"
|
||||
OLD_REV="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
git pull --ff-only 2>/dev/null || {
|
||||
error "git pull failed. Resolve conflicts: cd $DEPLOY_DIR && git status"
|
||||
exit 1
|
||||
}
|
||||
NEW_REV="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
if [[ "$OLD_REV" == "$NEW_REV" ]]; then
|
||||
info " Already up to date (no new commits)"
|
||||
else
|
||||
info " Updated: ${OLD_REV:0:8}..${NEW_REV:0:8}"
|
||||
fi
|
||||
else
|
||||
warn " Not a git repo. Skip git pull"
|
||||
fi
|
||||
|
||||
info "Step 2/4: Updating Python dependencies..."
|
||||
if [[ -d "$VENV_DIR/bin" ]]; then
|
||||
"$VENV_DIR/bin/pip" install -q --upgrade pip
|
||||
"$VENV_DIR/bin/pip" install -q -r "$DEPLOY_DIR/requirements.txt" 2>/dev/null || {
|
||||
warn " pip install had warnings, continuing..."
|
||||
}
|
||||
else
|
||||
error " venv not found at $VENV_DIR. Run install.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Step 3/4: Restarting Nexus..."
|
||||
supervisorctl restart nexus >/dev/null 2>&1 || {
|
||||
warn " supervisorctl restart failed. Try: supervisorctl restart nexus"
|
||||
}
|
||||
|
||||
info "Step 4/4: Verifying health..."
|
||||
PORT="$(nexus_publish_port_for "$DEPLOY_DIR")"
|
||||
HEALTH_OK=false
|
||||
for _ in $(seq 1 15); do
|
||||
if curl -sf "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
|
||||
HEALTH_OK=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [[ "$HEALTH_OK" == true ]]; then
|
||||
echo -e " ${GREEN}Upgrade complete! Nexus is healthy.${NC}"
|
||||
suggest_ops_cron "$DEPLOY_DIR"
|
||||
else
|
||||
echo -e " ${YELLOW}Health check pending. Verify manually:${NC}"
|
||||
echo " curl http://127.0.0.1:${PORT}/health"
|
||||
echo " supervisorctl status nexus"
|
||||
fi
|
||||
echo ""
|
||||
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify 1Panel + Docker install wizard readiness (run on the VPS as root).
|
||||
# See: deploy/README-1panel.md
|
||||
set -euo pipefail
|
||||
|
||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
||||
ENV_PROD="${NEXUS_ROOT}/docker/.env.prod"
|
||||
COMPOSE_FILE="${NEXUS_ROOT}/docker/docker-compose.prod.yml"
|
||||
COMPOSE_1PANEL="${NEXUS_ROOT}/docker/docker-compose.1panel.yml"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
pass() { echo -e "${GREEN}[PASS]${NC} $*"; }
|
||||
fail() { echo -e "${RED}[FAIL]${NC} $*"; FAILURES=$((FAILURES + 1)); }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; WARNS=$((WARNS + 1)); }
|
||||
info() { echo -e " $*"; }
|
||||
|
||||
FAILURES=0
|
||||
WARNS=0
|
||||
INSTALL_WIZARD_ARCHIVED=false
|
||||
PUBLISH_PORT=8600
|
||||
|
||||
require_root() {
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
fail "请用 root 运行(1Panel 终端默认 root)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_nexus_container() {
|
||||
NEXUS_CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
||||
if [[ -z "$NEXUS_CONTAINER" ]]; then
|
||||
fail "未找到运行中的 Nexus 容器"
|
||||
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
|
||||
return 1
|
||||
fi
|
||||
pass "Nexus 容器: $NEXUS_CONTAINER"
|
||||
}
|
||||
|
||||
check_repo() {
|
||||
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
|
||||
fail "仓库不存在: $NEXUS_ROOT"
|
||||
return 1
|
||||
fi
|
||||
local sha msg
|
||||
sha="$(git -C "$NEXUS_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
msg="$(git -C "$NEXUS_ROOT" log -1 --oneline 2>/dev/null || true)"
|
||||
pass "Git: $msg"
|
||||
if git -C "$NEXUS_ROOT" merge-base --is-ancestor b25d079 HEAD 2>/dev/null; then
|
||||
pass "已包含 b25d079(1Panel 容器名 sync 修复)"
|
||||
else
|
||||
warn "早于 b25d079 — 步骤 3 可能仍预填 host.docker.internal"
|
||||
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
|
||||
fi
|
||||
if git -C "$NEXUS_ROOT" merge-base --is-ancestor 3b2856f HEAD 2>/dev/null; then
|
||||
pass "已包含 3b2856f+(Redis :pass@ 自动解析,无 root)"
|
||||
else
|
||||
warn "早于 3b2856f — Redis 认证可能仍误用 root"
|
||||
info "修复: cd $NEXUS_ROOT && git pull && bash deploy/sync-install-wizard-to-container.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
check_env_prod() {
|
||||
if [[ ! -f "$ENV_PROD" ]]; then
|
||||
fail "缺少 $ENV_PROD"
|
||||
info "修复: bash $NEXUS_ROOT/deploy/install-nexus-fresh.sh --skip-clone"
|
||||
return 1
|
||||
fi
|
||||
pass "docker/.env.prod 存在"
|
||||
PUBLISH_PORT="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
PUBLISH_PORT="${PUBLISH_PORT:-8600}"
|
||||
pass "NEXUS_PUBLISH_PORT=$PUBLISH_PORT"
|
||||
if grep -qE '^NEXUS_INSTALL_WIZARD_PENDING=1' "$ENV_PROD"; then
|
||||
pass "NEXUS_INSTALL_WIZARD_PENDING=1(安装模式)"
|
||||
else
|
||||
warn "NEXUS_INSTALL_WIZARD_PENDING≠1 — entrypoint 可能已写 /app/.env"
|
||||
fi
|
||||
local db redis
|
||||
db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$db" ]]; then
|
||||
pass "NEXUS_1PANEL_DB_HOST=$db"
|
||||
else
|
||||
fail "未设置 NEXUS_1PANEL_DB_HOST"
|
||||
info "修复: bash $NEXUS_ROOT/deploy/detect-1panel-services.sh && nx update"
|
||||
fi
|
||||
if [[ -n "$redis" ]]; then
|
||||
pass "NEXUS_1PANEL_REDIS_HOST=$redis"
|
||||
local rurl
|
||||
rurl="$(grep -E '^NEXUS_REDIS_URL=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ "$rurl" == *"$redis"* ]]; then
|
||||
pass "NEXUS_REDIS_URL 已指向 Redis 容器"
|
||||
else
|
||||
warn "NEXUS_REDIS_URL 未含 $redis — nx update 会同步"
|
||||
fi
|
||||
else
|
||||
fail "未设置 NEXUS_1PANEL_REDIS_HOST"
|
||||
info "修复: 在 1Panel 应用商店安装 Redis 后执行 nx update"
|
||||
fi
|
||||
}
|
||||
|
||||
check_1panel_network() {
|
||||
if ! docker network inspect 1panel-network >/dev/null 2>&1; then
|
||||
fail "1panel-network 不存在"
|
||||
info "修复: 在 1Panel 应用商店安装 MySQL + Redis(会自动创建网络)"
|
||||
return 1
|
||||
fi
|
||||
pass "1panel-network 存在"
|
||||
if [[ -n "${NEXUS_CONTAINER:-}" ]]; then
|
||||
if docker inspect "$NEXUS_CONTAINER" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' \
|
||||
| grep -q '1panel-network'; then
|
||||
pass "Nexus 已接入 1panel-network"
|
||||
else
|
||||
fail "Nexus 未接入 1panel-network"
|
||||
info "修复: cd $NEXUS_ROOT && nx update --no-cache"
|
||||
fi
|
||||
fi
|
||||
if [[ -x "$NEXUS_ROOT/deploy/detect-1panel-services.sh" ]]; then
|
||||
info "探测结果:"
|
||||
"$NEXUS_ROOT/deploy/detect-1panel-services.sh" | sed 's/^/ /' || true
|
||||
fi
|
||||
}
|
||||
|
||||
probe_from_container() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
local db redis
|
||||
db="$(grep -E '^NEXUS_1PANEL_DB_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
redis="$(grep -E '^NEXUS_1PANEL_REDIS_HOST=' "$ENV_PROD" 2>/dev/null | cut -d= -f2- || true)"
|
||||
if [[ -n "$db" ]]; then
|
||||
if docker exec "$NEXUS_CONTAINER" python3 -c "
|
||||
import socket
|
||||
s=socket.socket(); s.settimeout(3); s.connect(('$db', 3306)); s.close()
|
||||
" 2>/dev/null; then
|
||||
pass "容器内 TCP → MySQL $db:3306"
|
||||
else
|
||||
fail "容器内无法连接 MySQL $db:3306"
|
||||
info "检查 MySQL 容器是否运行、Nexus 是否在 1panel-network"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$redis" ]]; then
|
||||
if docker exec "$NEXUS_CONTAINER" python3 -c "
|
||||
import socket
|
||||
s=socket.socket(); s.settimeout(3); s.connect(('$redis', 6379)); s.close()
|
||||
" 2>/dev/null; then
|
||||
pass "容器内 TCP → Redis $redis:6379"
|
||||
else
|
||||
fail "容器内无法连接 Redis $redis:6379"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_app_env_redis_url() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
if ! docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
local rurl
|
||||
rurl="$(docker exec "$NEXUS_CONTAINER" grep -E '^NEXUS_REDIS_URL=' /app/.env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
|
||||
if [[ -z "$rurl" ]]; then
|
||||
warn "/app/.env 无 NEXUS_REDIS_URL"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$rurl" == *"://root:"* ]]; then
|
||||
fail "NEXUS_REDIS_URL 含 root 用户(1Panel Redis 无 root 账号)"
|
||||
info "修复: redis://:密码@1Panel-redis-xxxx:6379/0 — 见 deploy/test-1panel-redis.sh"
|
||||
elif [[ "$rurl" == *"1Panel-redis"* ]] || [[ "$rurl" == *"://:"* ]]; then
|
||||
pass "NEXUS_REDIS_URL 格式合理(容器名或 :pass@)"
|
||||
else
|
||||
warn "NEXUS_REDIS_URL 可能未指向 1Panel Redis: $rurl"
|
||||
fi
|
||||
}
|
||||
|
||||
check_install_mode() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
if docker exec "$NEXUS_CONTAINER" test -f /app/.env 2>/dev/null; then
|
||||
warn "/app/.env 已存在 — 安装向导可能已进入步骤 3+ 或误写 env"
|
||||
check_app_env_redis_url
|
||||
info "若需重来: 删卷 nexus-state 中 .env 并设 NEXUS_INSTALL_WIZARD_PENDING=1 后 nx update"
|
||||
else
|
||||
pass "/app/.env 不存在(安装模式 OK)"
|
||||
fi
|
||||
if docker exec "$NEXUS_CONTAINER" test -f /app/.install_locked 2>/dev/null \
|
||||
|| docker exec "$NEXUS_CONTAINER" test -f /var/lib/nexus/.install_locked 2>/dev/null \
|
||||
|| docker exec "$NEXUS_CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null; then
|
||||
pass "安装向导已锁定/已归档 — 应访问 /app/ 登录"
|
||||
INSTALL_WIZARD_ARCHIVED=true
|
||||
fi
|
||||
local hosts_json
|
||||
hosts_json="$(docker exec "$NEXUS_CONTAINER" cat /app/web/data/1panel-hosts.json 2>/dev/null || true)"
|
||||
if [[ -n "$hosts_json" ]]; then
|
||||
pass "1panel-hosts.json: $hosts_json"
|
||||
else
|
||||
warn "无 1panel-hosts.json(依赖容器环境变量,nx update 会写入)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_ops_cron() {
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" != true ]]; then
|
||||
return 0
|
||||
fi
|
||||
if ! command -v crontab >/dev/null 2>&1; then
|
||||
warn "未安装 cron/crontab — Layer 3 宿主机巡检不可用"
|
||||
info "修复: sudo bash $NEXUS_ROOT/deploy/install_ops_cron.sh"
|
||||
return 0
|
||||
fi
|
||||
local cron
|
||||
cron="$(crontab -l 2>/dev/null || true)"
|
||||
if echo "$cron" | grep -q 'nexus-health-monitor\|deploy/health_monitor.sh'; then
|
||||
pass "Crontab 含 health_monitor"
|
||||
else
|
||||
warn "Crontab 未配置 health_monitor(Layer 3 巡检)"
|
||||
info "修复: sudo nx cron"
|
||||
fi
|
||||
if echo "$cron" | grep -q 'nexus-mysql-backup\|deploy/db_backup.sh'; then
|
||||
pass "Crontab 含 db_backup"
|
||||
else
|
||||
warn "Crontab 未配置 db_backup(每日 MySQL 备份)"
|
||||
info "修复: sudo nx cron"
|
||||
fi
|
||||
}
|
||||
|
||||
check_mysql_backup() {
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" != true ]]; then
|
||||
return 0
|
||||
fi
|
||||
local dump_sh="${NEXUS_ROOT}/deploy/mysql_dump_to_file.sh"
|
||||
if [[ -f "$dump_sh" ]]; then
|
||||
pass "mysql_dump_to_file.sh 存在"
|
||||
else
|
||||
warn "缺少 mysql_dump_to_file.sh — 升级前无法备份 MySQL"
|
||||
info "修复: cd $NEXUS_ROOT && git pull && nx update"
|
||||
return 0
|
||||
fi
|
||||
if [[ -d /var/backups/nexus ]] && ls /var/backups/nexus/nexus_*.sql.gz >/dev/null 2>&1; then
|
||||
local latest
|
||||
latest="$(ls -t /var/backups/nexus/nexus_*.sql.gz 2>/dev/null | head -1)"
|
||||
pass "已有 MySQL 备份: $(basename "$latest")"
|
||||
else
|
||||
warn "尚无 MySQL 备份文件(/var/backups/nexus)"
|
||||
info "可手动: sudo bash $NEXUS_ROOT/deploy/db_backup.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
check_http_local() {
|
||||
local health code
|
||||
health="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/health" 2>/dev/null || true)"
|
||||
if [[ "$health" == "ok" ]]; then
|
||||
pass "/health → ok"
|
||||
else
|
||||
fail "/health 非 ok(当前: ${health:-无响应})"
|
||||
info "修复: docker logs --tail=80 $NEXUS_CONTAINER"
|
||||
return 1
|
||||
fi
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PUBLISH_PORT}/app/install.html" 2>/dev/null || echo 000)"
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
|
||||
if [[ "$code" == "404" ]]; then
|
||||
pass "/app/install.html 已归档 → HTTP 404"
|
||||
else
|
||||
fail "/app/install.html 应已归档但 HTTP $code"
|
||||
info "修复: nx update(会执行 archive_install_wizard_in_container)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [[ "$code" == "200" ]]; then
|
||||
pass "/app/install.html → HTTP 200"
|
||||
else
|
||||
fail "/app/install.html → HTTP $code"
|
||||
info "修复: bash $NEXUS_ROOT/deploy/sync-install-wizard-to-container.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
check_install_api() {
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
|
||||
pass "安装 API 已关闭(向导已锁定,/api/install/* 返回 403 为预期)"
|
||||
return 0
|
||||
fi
|
||||
local status env_json
|
||||
status="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/api/install/status" 2>/dev/null || true)"
|
||||
if [[ -n "$status" ]]; then
|
||||
pass "/api/install/status → $status"
|
||||
if echo "$status" | grep -q '"installed":true'; then
|
||||
warn "installed=true — 向导已完成,请访问 /app/ 登录"
|
||||
fi
|
||||
else
|
||||
fail "/api/install/status 无响应"
|
||||
fi
|
||||
env_json="$(curl -sf "http://127.0.0.1:${PUBLISH_PORT}/api/install/env-check" 2>/dev/null || true)"
|
||||
if [[ -z "$env_json" ]]; then
|
||||
fail "/api/install/env-check 无响应"
|
||||
return 1
|
||||
fi
|
||||
if echo "$env_json" | grep -q 'docker_defaults'; then
|
||||
local db_host
|
||||
db_host="$(echo "$env_json" | python3 -c "
|
||||
import json,sys
|
||||
d=json.load(sys.stdin)
|
||||
print((d.get('docker_defaults') or {}).get('db_host',''))
|
||||
" 2>/dev/null || true)"
|
||||
if [[ -n "$db_host" && "$db_host" != "host.docker.internal" ]]; then
|
||||
pass "步骤 3 预填 db_host=$db_host"
|
||||
elif [[ "$db_host" == "host.docker.internal" ]]; then
|
||||
fail "步骤 3 仍预填 host.docker.internal"
|
||||
info "修复: nx update --no-cache(需 b25d079+)"
|
||||
fi
|
||||
else
|
||||
warn "env-check 无 docker_defaults(非 Docker 模式?)"
|
||||
fi
|
||||
if echo "$env_json" | grep -q '1Panel-mysql'; then
|
||||
pass "env-check MySQL 探测含 1Panel 容器名"
|
||||
elif echo "$env_json" | grep -q 'host.docker.internal'; then
|
||||
warn "env-check MySQL 仍指向 host.docker.internal"
|
||||
fi
|
||||
}
|
||||
|
||||
check_container_image_features() {
|
||||
[[ -n "${NEXUS_CONTAINER:-}" ]] || return 0
|
||||
local host_sha py_ok html_file html_ok host_root_ok
|
||||
host_sha="$(git -C "$NEXUS_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
pass "宿主机 Git: $host_sha"
|
||||
|
||||
if docker exec "$NEXUS_CONTAINER" grep -q '_host_deploy_root' /app/server/api/install.py 2>/dev/null; then
|
||||
pass "容器 install.py 含 host_deploy_root(round6+)"
|
||||
py_ok=1
|
||||
else
|
||||
warn "容器 install.py 较旧 — 需 nx update 重建镜像"
|
||||
fi
|
||||
|
||||
html_file="/app/web/app/install.html"
|
||||
if docker exec "$NEXUS_CONTAINER" test -f /app/web/app/install.html.bak 2>/dev/null; then
|
||||
html_file="/app/web/app/install.html.bak"
|
||||
fi
|
||||
if docker exec "$NEXUS_CONTAINER" grep -q 'install_ops_cron' "$html_file" 2>/dev/null; then
|
||||
pass "容器 install.html 含 Layer 3 cron 指引(round7+)"
|
||||
html_ok=1
|
||||
else
|
||||
warn "容器 install.html 较旧 — 需 nx update 重建镜像"
|
||||
fi
|
||||
|
||||
host_root_ok="$(docker exec "$NEXUS_CONTAINER" printenv NEXUS_HOST_ROOT 2>/dev/null || true)"
|
||||
if [[ -n "$host_root_ok" ]]; then
|
||||
pass "NEXUS_HOST_ROOT=$host_root_ok"
|
||||
else
|
||||
warn "容器未注入 NEXUS_HOST_ROOT — nx update 重建后可修复"
|
||||
fi
|
||||
|
||||
if [[ -n "${py_ok:-}" && -n "${html_ok:-}" ]]; then
|
||||
info "镜像与宿主机代码特性对齐(重建后验收)"
|
||||
else
|
||||
info "修复: cd $NEXUS_ROOT && sudo nx update"
|
||||
fi
|
||||
}
|
||||
|
||||
check_openresty_hint() {
|
||||
local domain code
|
||||
domain="$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo api.synaglobal.vip)"
|
||||
code="$(curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 5 "https://${domain}/health" 2>/dev/null || echo 000)"
|
||||
if [[ "$code" == "200" ]] && curl -sk "https://${domain}/health" 2>/dev/null | grep -q '^ok$'; then
|
||||
pass "HTTPS https://${domain}/health → ok"
|
||||
else
|
||||
warn "HTTPS https://${domain}/health → HTTP $code(外网可能未配 OpenResty 反代)"
|
||||
info "1Panel → 网站 → 反代 http://127.0.0.1:8600"
|
||||
info "参考: $NEXUS_ROOT/deploy/1panel/openresty-nexus.conf.example"
|
||||
fi
|
||||
}
|
||||
|
||||
print_next_steps() {
|
||||
local domain
|
||||
domain="$(grep -E '^NEXUS_API_BASE_URL=' "$ENV_PROD" 2>/dev/null | sed 's|.*https\?://||;s|/.*||' || echo '你的域名')"
|
||||
echo ""
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
|
||||
echo "=========================================="
|
||||
echo " 已安装 — 日常运维"
|
||||
echo "=========================================="
|
||||
echo " 登录: https://${domain}/app/"
|
||||
echo " 更新: cd $NEXUS_ROOT && nx update"
|
||||
echo " 巡检 cron: sudo nx cron"
|
||||
echo " 手动备份: sudo bash $NEXUS_ROOT/deploy/db_backup.sh"
|
||||
echo " 验收: bash $NEXUS_ROOT/deploy/verify-1panel-install-wizard.sh"
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
echo "=========================================="
|
||||
echo " 安装向导后续步骤"
|
||||
echo "=========================================="
|
||||
echo "1. 1Panel 数据库 → 建库 nexus、用户 nexus(仅授权 nexus 库)"
|
||||
echo "2. 浏览器打开: https://${domain}/app/install.html"
|
||||
echo "3. 步骤 2:MySQL/Redis TCP 应显示 ✓(容器名:端口)"
|
||||
echo "4. 步骤 3:确认 db_host/redis_host 为 1Panel-mysql/redis-xxxx,填 MySQL 密码"
|
||||
echo "5. 步骤 4:连接检测全绿 → 创建 admin"
|
||||
echo "6. 步骤 5:锁定安装"
|
||||
echo ""
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
echo "先修复上方 [FAIL],再执行:"
|
||||
echo " cd $NEXUS_ROOT && nx update --no-cache"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
require_root
|
||||
echo "Nexus 1Panel 安装向导验收 — $(date -Iseconds)"
|
||||
echo "NEXUS_ROOT=$NEXUS_ROOT"
|
||||
echo ""
|
||||
|
||||
check_repo || true
|
||||
check_env_prod || true
|
||||
resolve_nexus_container || true
|
||||
check_1panel_network || true
|
||||
probe_from_container || true
|
||||
check_install_mode || true
|
||||
check_http_local || true
|
||||
check_install_api || true
|
||||
check_ops_cron || true
|
||||
check_mysql_backup || true
|
||||
check_container_image_features || true
|
||||
check_openresty_hint || true
|
||||
print_next_steps
|
||||
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
echo ""
|
||||
fail "合计 $FAILURES 项未通过"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
if [[ "${INSTALL_WIZARD_ARCHIVED:-false}" == true ]]; then
|
||||
pass "验收通过 — 安装已完成,请访问 /app/ 登录"
|
||||
if [[ "$WARNS" -gt 0 ]]; then
|
||||
warn "有 ${WARNS} 项 [WARN](cron/备份等建议项,不阻断登录)"
|
||||
fi
|
||||
else
|
||||
pass "验收通过 — 可打开安装向导完成步骤 1–5"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Post-deploy smoke: login + optional batch endpoint (run on server)."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
BASE = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600")
|
||||
USER = os.environ.get("NEXUS_TEST_ADMIN_USER", "admin")
|
||||
PASSWORD = os.environ.get("NEXUS_TEST_ADMIN_PASSWORD", "")
|
||||
|
||||
|
||||
def post(path: str, body: dict, headers: dict | None = None) -> dict:
|
||||
h = {"Content-Type": "application/json", **(headers or {})}
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}{path}",
|
||||
data=json.dumps(body).encode(),
|
||||
headers=h,
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.load(resp)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not PASSWORD:
|
||||
print("SKIP: set NEXUS_TEST_ADMIN_PASSWORD", file=sys.stderr)
|
||||
return 2
|
||||
login = post("/api/auth/login", {"username": USER, "password": PASSWORD})
|
||||
token = login.get("access_token")
|
||||
exp = login.get("expires_in")
|
||||
print(f"login ok expires_in={exp}")
|
||||
if exp != 3600:
|
||||
print(f"WARN: expected expires_in=3600, got {exp}")
|
||||
if not token:
|
||||
print("FAIL: no access_token")
|
||||
return 1
|
||||
# Minimal authenticated call
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/api/servers/stats",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
stats = json.load(resp)
|
||||
print(f"servers/stats ok keys={list(stats.keys())[:5]}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"HTTP {e.code}: {e.read().decode()[:500]}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,89 @@
|
||||
# Nexus 6.0 — Docker Compose (dev / staging)
|
||||
# Usage:
|
||||
# cp docker/.env.example docker/.env # or: python docker/generate_env.py
|
||||
# docker compose up -d --build
|
||||
# open http://localhost:8600/app/
|
||||
|
||||
name: nexus
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-nexus_root_dev}
|
||||
MYSQL_DATABASE: nexus
|
||||
MYSQL_USER: nexus
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-nexus_dev_pass}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
start_period: 40s
|
||||
ports:
|
||||
- "${MYSQL_PUBLISH_PORT:-3306}:3306"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 8
|
||||
ports:
|
||||
- "${REDIS_PUBLISH_PORT:-6379}:6379"
|
||||
|
||||
nexus:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "${NEXUS_PUBLISH_PORT:-8600}:8600"
|
||||
environment:
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_PORT: "3306"
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: "6379"
|
||||
NEXUS_PERSIST_DIR: /var/lib/nexus
|
||||
NEXUS_DOCKER_WRITE_ENV: "1"
|
||||
NEXUS_HOST: "0.0.0.0"
|
||||
NEXUS_PORT: "8600"
|
||||
NEXUS_DEPLOY_PATH: /app
|
||||
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD:-nexus_dev_pass}@mysql:3306/nexus
|
||||
NEXUS_REDIS_URL: redis://redis:6379/0
|
||||
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:-http://localhost:8600,http://127.0.0.1:8600}
|
||||
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:-}
|
||||
NEXUS_API_KEY: ${NEXUS_API_KEY:-}
|
||||
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:-}
|
||||
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:-http://localhost:8600}
|
||||
env_file:
|
||||
- path: docker/.env
|
||||
required: false
|
||||
volumes:
|
||||
- nexus-state:/var/lib/nexus
|
||||
- nexus-web-data:/app/web/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://127.0.0.1:8600/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 90s
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
redis-data:
|
||||
nexus-state:
|
||||
nexus-web-data:
|
||||
@@ -0,0 +1,18 @@
|
||||
# Copy to docker/.env (or run: python docker/generate_env.py)
|
||||
# Used by docker compose for nexus service — never commit docker/.env
|
||||
|
||||
MYSQL_ROOT_PASSWORD=change_me_root
|
||||
MYSQL_PASSWORD=change_me_nexus
|
||||
|
||||
# Leave empty to use install wizard at http://localhost:8600/app/install.html
|
||||
# Or run generate_env.py to fill these three:
|
||||
NEXUS_SECRET_KEY=
|
||||
NEXUS_API_KEY=
|
||||
NEXUS_ENCRYPTION_KEY=
|
||||
|
||||
NEXUS_CORS_ORIGINS=http://localhost:8600,http://127.0.0.1:8600
|
||||
NEXUS_API_BASE_URL=http://localhost:8600
|
||||
|
||||
# Optional host port overrides
|
||||
# NEXUS_PUBLISH_PORT=8600
|
||||
# MYSQL_PUBLISH_PORT=3306
|
||||
@@ -0,0 +1,39 @@
|
||||
# Production Compose env — copy to docker/.env.prod (never commit)
|
||||
# Fresh install: bash deploy/install-nexus-fresh.sh (auto-generates NEXUS keys per host)
|
||||
# MySQL / Redis 不在 Docker 栈 — 请在宿主机或 1Panel 自建,安装向导步骤 3 填写
|
||||
|
||||
# Auto-generated on fresh install (unique per server). Do NOT copy between hosts.
|
||||
NEXUS_SECRET_KEY=
|
||||
NEXUS_API_KEY=
|
||||
NEXUS_ENCRYPTION_KEY=
|
||||
|
||||
# 1 = entrypoint must not write /app/.env until install wizard step 3
|
||||
NEXUS_INSTALL_WIZARD_PENDING=0
|
||||
|
||||
# Public URL
|
||||
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
|
||||
NEXUS_API_BASE_URL=https://api.synaglobal.vip
|
||||
|
||||
# Nexus container resource profile (docker/profiles/*.env)
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
|
||||
# 1Panel:nx update 自动探测并写入(安装向导步骤 3 预填)
|
||||
# NEXUS_1PANEL_DB_HOST=1Panel-mysql-xxxx
|
||||
# NEXUS_1PANEL_REDIS_HOST=1Panel-redis-xxxx
|
||||
|
||||
# 宿主机 Compose 插值/ nx update 同步用;安装向导完成后以卷内 /app/.env 为准(不经 Compose 注入)
|
||||
# 1Panel 有密码时: redis://:你的密码@1Panel-redis-xxxx:6379/0
|
||||
NEXUS_REDIS_URL=redis://host.docker.internal:6379/0
|
||||
|
||||
# 宿主机 git 仓库路径(容器内安装向导用于提示 nx cron / nx update)
|
||||
NEXUS_HOST_ROOT=/opt/nexus
|
||||
|
||||
# Optional — production probe / E2E only (host .env.prod; NOT for API runtime; never commit values)
|
||||
# NEXUS_TEST_ADMIN_PASSWORD — must match admin login password
|
||||
# NEXUS_PROBE_CLIENT_IP — optional; IP in login allowlist for origin probe (auto-read from DB if unset)
|
||||
# NEXUS_TEST_ADMIN_PASSWORD=
|
||||
# NEXUS_PROBE_CLIENT_IP=
|
||||
|
||||
# Optional
|
||||
# NEXUS_PUBLISH_PORT=8600
|
||||
@@ -0,0 +1,71 @@
|
||||
# Nexus Docker
|
||||
|
||||
## 前置
|
||||
|
||||
- [Docker Desktop](https://www.docker.com/products/docker-desktop/)(Windows/macOS)或 Docker Engine + Compose v2
|
||||
|
||||
## 快速启动(正常模式)
|
||||
|
||||
```bash
|
||||
# 1. 生成密钥(写入 docker/.env,已 gitignore)
|
||||
python docker/generate_env.py
|
||||
|
||||
# 2. 构建并启动
|
||||
docker compose up -d --build
|
||||
|
||||
# 3. 健康检查
|
||||
curl -s http://localhost:8600/health # 期望: ok
|
||||
|
||||
# 4. 打开 SPA
|
||||
# http://localhost:8600/app/
|
||||
```
|
||||
|
||||
默认管理员需在安装向导或已有 DB 中配置;若 DB 为空且未跑过 install,可删卷后走安装模式。
|
||||
|
||||
## 安装向导模式
|
||||
|
||||
不创建 `docker/.env`(或清空 `NEXUS_SECRET_KEY`)时,应用以 **安装模式** 启动:
|
||||
|
||||
- http://localhost:8600/app/install.html
|
||||
|
||||
完成向导后 `.env` 会写入容器;`nexus-state` 卷会在重启时恢复配置。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
docker compose logs -f nexus
|
||||
docker compose down # 停止,保留数据卷
|
||||
docker compose down -v # 停止并删除数据卷(清空 DB)
|
||||
docker compose build --no-cache nexus
|
||||
```
|
||||
|
||||
## 服务
|
||||
|
||||
| 环境 | Compose 服务 | 说明 |
|
||||
|------|-------------|------|
|
||||
| 本地开发 `docker-compose.yml` | nexus + mysql + redis | 一键本地联调 |
|
||||
| 生产 `docker-compose.prod.yml` | **仅 nexus** | MySQL/Redis 宿主机或 1Panel 自建 |
|
||||
|
||||
生产 1Panel:`docker/.env.prod` 可含 `NEXUS_REDIS_URL`(供 `nx update` 探测同步),**不经 Compose 注入容器**;运行时 Redis/MySQL URL 以安装向导写入的 `/app/.env`(`nexus-state` 卷)为准。MySQL `NEXUS_DATABASE_URL` 仅由向导步骤 3 写入。
|
||||
|
||||
## 生产注意
|
||||
|
||||
- 修改 `NEXUS_*` 为强密码;MySQL 使用自建库强密码。
|
||||
- 建议前置 Nginx/TLS,仅暴露 443。
|
||||
- 多实例部署需共享 MySQL、Redis;后台任务由 Redis primary lock 保证单实例执行。
|
||||
|
||||
### 1Panel 生产(Compose + OpenResty)
|
||||
|
||||
```bash
|
||||
cp docker/.env.prod.example docker/.env.prod # 从宝塔 .env 迁入密钥
|
||||
docker compose -f docker/docker-compose.prod.yml --env-file docker/.env.prod up -d --build
|
||||
```
|
||||
|
||||
- 镜像:`Dockerfile.prod`(含 `npm ci` + `vite build` + **`web/app/install.html` 安装向导**)
|
||||
- 仅绑定 `127.0.0.1:8600`;OpenResty 反代见 `deploy/1panel/openresty-nexus.conf.example`
|
||||
- **2C8G 调优**:`docker/profiles/2c8g.env`(`NEXUS_MEM_LIMIT` + `NEXUS_DB_POOL_SIZE=30`);外置 MySQL 可参考 `docker/mysql-prod-2c8g.cnf` 手工调优
|
||||
- 完整迁移步骤:`docs/design/plans/2026-06-04-1panel-docker-production.md`
|
||||
- 一键安装:`bash deploy/install-1panel-docker.sh`
|
||||
- 一键升级:`bash deploy/upgrade-1panel-docker.sh`(`--check` 仅查更新)
|
||||
|
||||
详见 `docs/design/specs/2026-06-03-docker-design.md`。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 1Panel overlay — join Nexus to 1panel-network (App Store MySQL/Redis use this network).
|
||||
# Applied automatically when `docker network inspect 1panel-network` succeeds (deploy/nexus-1panel.sh).
|
||||
|
||||
services:
|
||||
nexus:
|
||||
networks:
|
||||
- 1panel-network
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
name: 1panel-network
|
||||
@@ -0,0 +1,61 @@
|
||||
# Nexus 6.0 — production Compose (1Panel / bare Docker host)
|
||||
# MySQL / Redis 不在 Compose 内 — 请在宿主机或 1Panel 自行安装
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/nexus-1panel.sh install --profile 2c8g
|
||||
# docker compose -f docker/docker-compose.prod.yml \
|
||||
# --env-file docker/.env.prod --env-file docker/profiles/2c8g.env up -d --build
|
||||
#
|
||||
# OpenResty on host proxies https://api.synaglobal.vip → 127.0.0.1:8600
|
||||
|
||||
name: nexus-prod
|
||||
|
||||
services:
|
||||
nexus:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.prod
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "127.0.0.1:${NEXUS_PUBLISH_PORT:-8600}:8600"
|
||||
environment:
|
||||
NEXUS_PERSIST_DIR: /var/lib/nexus
|
||||
NEXUS_DOCKER_WRITE_ENV: "1"
|
||||
NEXUS_HOST: "0.0.0.0"
|
||||
NEXUS_PORT: "8600"
|
||||
NEXUS_DEPLOY_PATH: /app
|
||||
NEXUS_HOST_ROOT: ${NEXUS_HOST_ROOT:-/opt/nexus}
|
||||
# Redis URL 仅来自 /app/.env(安装向导 + entrypoint export_app_env),勿在此注入以免覆盖密码
|
||||
NEXUS_1PANEL_DB_HOST: ${NEXUS_1PANEL_DB_HOST:-}
|
||||
NEXUS_1PANEL_REDIS_HOST: ${NEXUS_1PANEL_REDIS_HOST:-}
|
||||
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:?set NEXUS_CORS_ORIGINS}
|
||||
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:?set NEXUS_SECRET_KEY}
|
||||
NEXUS_API_KEY: ${NEXUS_API_KEY:?set NEXUS_API_KEY}
|
||||
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:?set NEXUS_ENCRYPTION_KEY}
|
||||
NEXUS_INSTALL_WIZARD_PENDING: ${NEXUS_INSTALL_WIZARD_PENDING:-0}
|
||||
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:?set NEXUS_API_BASE_URL}
|
||||
NEXUS_DB_POOL_SIZE: ${NEXUS_DB_POOL_SIZE:-30}
|
||||
NEXUS_DB_MAX_OVERFLOW: ${NEXUS_DB_MAX_OVERFLOW:-20}
|
||||
volumes:
|
||||
- nexus-state:/var/lib/nexus
|
||||
- nexus-web-data:/app/web/data
|
||||
mem_limit: ${NEXUS_MEM_LIMIT:-2g}
|
||||
cpus: ${NEXUS_CPUS:-1.5}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
networks:
|
||||
- nexus-internal
|
||||
|
||||
networks:
|
||||
nexus-internal:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
nexus-state:
|
||||
nexus-web-data:
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/bin/sh
|
||||
# Nexus Docker entrypoint — wait for deps, sync .env with persistent volume, exec uvicorn.
|
||||
set -eu
|
||||
|
||||
PERSIST_DIR="${NEXUS_PERSIST_DIR:-/var/lib/nexus}"
|
||||
PERSIST_ENV="${PERSIST_DIR}/.env"
|
||||
|
||||
wait_tcp() {
|
||||
host="$1"
|
||||
port="$2"
|
||||
echo "entrypoint: waiting for ${host}:${port}..."
|
||||
until python3 -c "
|
||||
import socket
|
||||
s = socket.socket()
|
||||
s.settimeout(2)
|
||||
s.connect(('${host}', ${port}))
|
||||
s.close()
|
||||
" 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
wait_tcp_optional() {
|
||||
host="$1"
|
||||
port="$2"
|
||||
tries="${3:-15}"
|
||||
label="${4:-service}"
|
||||
echo "entrypoint: waiting for ${label} ${host}:${port} (max ${tries} tries)..."
|
||||
n=0
|
||||
while [ "$n" -lt "$tries" ]; do
|
||||
if python3 -c "
|
||||
import socket
|
||||
s = socket.socket()
|
||||
s.settimeout(2)
|
||||
s.connect(('${host}', ${port}))
|
||||
s.close()
|
||||
" 2>/dev/null; then
|
||||
echo "entrypoint: ${label} ready at ${host}:${port}"
|
||||
return 0
|
||||
fi
|
||||
n=$((n + 1))
|
||||
sleep 2
|
||||
done
|
||||
echo "entrypoint: warn — ${label} not reachable at ${host}:${port}; install on host or set URL in .env"
|
||||
return 0
|
||||
}
|
||||
|
||||
restore_persisted_install_lock() {
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
PERSIST_LOCK="${PERSIST_DIR}/.install_locked"
|
||||
if [ -f "${PERSIST_LOCK}" ] && [ ! -f /app/.install_locked ]; then
|
||||
cp "${PERSIST_LOCK}" /app/.install_locked
|
||||
chmod 600 /app/.install_locked
|
||||
echo "entrypoint: restored .install_locked from ${PERSIST_LOCK}"
|
||||
fi
|
||||
}
|
||||
|
||||
archive_install_wizard_if_locked() {
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" = "1" ]; then
|
||||
rm -f /app/.install_locked "${PERSIST_DIR}/.install_locked" 2>/dev/null || true
|
||||
if [ -f /app/web/app/install.html.bak ] && [ ! -f /app/web/app/install.html ]; then
|
||||
mv /app/web/app/install.html.bak /app/web/app/install.html
|
||||
echo "entrypoint: restored install.html from .bak (wizard pending)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f /app/.install_locked ] && [ ! -f "${PERSIST_DIR}/.install_locked" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -f /app/web/app/install.html.bak ]; then
|
||||
if [ -f /app/web/app/install.html ]; then
|
||||
rm -f /app/web/app/install.html
|
||||
echo "entrypoint: removed install.html (wizard archived as install.html.bak)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [ -f /app/web/app/install.html ]; then
|
||||
mv /app/web/app/install.html /app/web/app/install.html.bak
|
||||
echo "entrypoint: archived install.html → install.html.bak"
|
||||
fi
|
||||
}
|
||||
|
||||
restore_persisted_env() {
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "${PERSIST_ENV}" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! grep -q '^NEXUS_DATABASE_URL=' "${PERSIST_ENV}" 2>/dev/null; then
|
||||
echo "entrypoint: warn — ${PERSIST_ENV} missing NEXUS_DATABASE_URL; skip restore (complete install wizard)"
|
||||
return 0
|
||||
fi
|
||||
cp "${PERSIST_ENV}" /app/.env
|
||||
chmod 600 /app/.env
|
||||
echo "entrypoint: restored .env from ${PERSIST_ENV}"
|
||||
}
|
||||
|
||||
# Compose may inject placeholder NEXUS_REDIS_URL (no password). Wizard .env wins.
|
||||
export_app_env() {
|
||||
if [ ! -f /app/.env ]; then
|
||||
return 0
|
||||
fi
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. /app/.env
|
||||
set +a
|
||||
echo "entrypoint: exported NEXUS_* from /app/.env (overrides Compose placeholders)"
|
||||
}
|
||||
|
||||
write_env_from_environment() {
|
||||
if [ -f /app/.env ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "${NEXUS_DOCKER_WRITE_ENV:-}" != "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -z "${NEXUS_SECRET_KEY:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-}" = "1" ]; then
|
||||
echo "entrypoint: install wizard pending — skip writing /app/.env"
|
||||
return 0
|
||||
fi
|
||||
if [ -z "${NEXUS_DATABASE_URL:-}" ]; then
|
||||
echo "entrypoint: skip writing /app/.env — NEXUS_DATABASE_URL not set (use /app/install.html)"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "${PERSIST_DIR}"
|
||||
{
|
||||
echo "# Generated by docker/entrypoint.sh — do not commit"
|
||||
env | grep '^NEXUS_' | sort
|
||||
} > /app/.env
|
||||
chmod 600 /app/.env
|
||||
cp /app/.env "${PERSIST_ENV}"
|
||||
echo "entrypoint: wrote .env from container environment"
|
||||
}
|
||||
|
||||
persist_state_on_exit() {
|
||||
if [ -f /app/.env ]; then
|
||||
mkdir -p "${PERSIST_DIR}"
|
||||
cp /app/.env "${PERSIST_ENV}"
|
||||
chmod 600 "${PERSIST_ENV}" 2>/dev/null || true
|
||||
fi
|
||||
if [ -f /app/.install_locked ]; then
|
||||
mkdir -p "${PERSIST_DIR}"
|
||||
cp /app/.install_locked "${PERSIST_DIR}/.install_locked"
|
||||
chmod 600 "${PERSIST_DIR}/.install_locked" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap persist_state_on_exit EXIT INT TERM
|
||||
|
||||
# MySQL / Redis 均由用户自行安装在宿主机;安装向导阶段不阻塞启动
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_MYSQL_WAIT:-0}" != "1" ]; then
|
||||
MYSQL_HOST="${NEXUS_1PANEL_DB_HOST:-${MYSQL_HOST:-host.docker.internal}}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
wait_tcp_optional "${MYSQL_HOST}" "${MYSQL_PORT}" 30 "MySQL"
|
||||
fi
|
||||
|
||||
if [ "${NEXUS_INSTALL_WIZARD_PENDING:-0}" != "1" ] && [ "${NEXUS_SKIP_REDIS_WAIT:-0}" != "1" ]; then
|
||||
REDIS_HOST="${NEXUS_1PANEL_REDIS_HOST:-${REDIS_HOST:-host.docker.internal}}"
|
||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||
wait_tcp_optional "${REDIS_HOST}" "${REDIS_PORT}" 15 "Redis"
|
||||
fi
|
||||
|
||||
restore_persisted_install_lock
|
||||
archive_install_wizard_if_locked
|
||||
restore_persisted_env
|
||||
write_env_from_environment
|
||||
export_app_env
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate docker/.env with random secrets for local Compose."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.generate_nexus_secrets import ( # noqa: E402
|
||||
generate_nexus_secrets,
|
||||
write_env_prod,
|
||||
)
|
||||
|
||||
OUT = ROOT / "docker" / ".env"
|
||||
EXAMPLE = ROOT / "docker" / ".env.example"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if OUT.exists():
|
||||
raise SystemExit(f"Refusing to overwrite existing {OUT} — delete it first.")
|
||||
|
||||
secrets_map = generate_nexus_secrets()
|
||||
write_env_prod(
|
||||
EXAMPLE,
|
||||
OUT,
|
||||
domain="localhost",
|
||||
secrets_map=secrets_map,
|
||||
profile_env=None,
|
||||
wizard_pending=False,
|
||||
)
|
||||
print(f"Wrote {OUT}")
|
||||
print("Next: docker compose up -d --build")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,20 @@
|
||||
# MySQL 8.4 — Nexus production tuning for 1 vCPU / 4 GiB host (minimal)
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
innodb_buffer_pool_size = 512M
|
||||
innodb_log_file_size = 128M
|
||||
|
||||
max_connections = 80
|
||||
|
||||
innodb_read_io_threads = 2
|
||||
innodb_write_io_threads = 2
|
||||
|
||||
table_open_cache = 1000
|
||||
tmp_table_size = 32M
|
||||
max_heap_table_size = 32M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
@@ -0,0 +1,25 @@
|
||||
# MySQL 8.4 — Nexus production tuning for 2 vCPU / 8 GiB host
|
||||
# Mounted by docker/docker-compose.prod.yml (see docs/design/plans/2026-06-04-1panel-docker-production.md)
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
# ~1 GiB for InnoDB (leave RAM for Nexus, Redis, 1Panel, OS)
|
||||
innodb_buffer_pool_size = 1024M
|
||||
innodb_log_file_size = 256M
|
||||
|
||||
# Nexus async pool default 30+20; leave headroom for admin/migrations
|
||||
max_connections = 120
|
||||
|
||||
# 2 vCPU: avoid aggressive IO threads
|
||||
innodb_read_io_threads = 4
|
||||
innodb_write_io_threads = 4
|
||||
|
||||
# Sensible defaults on small VMs
|
||||
table_open_cache = 2000
|
||||
tmp_table_size = 64M
|
||||
max_heap_table_size = 64M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
@@ -0,0 +1,20 @@
|
||||
# MySQL 8.4 — Nexus production tuning for 4 vCPU / 16 GiB host
|
||||
|
||||
[mysqld]
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
innodb_buffer_pool_size = 4096M
|
||||
innodb_log_file_size = 512M
|
||||
|
||||
max_connections = 200
|
||||
|
||||
innodb_read_io_threads = 8
|
||||
innodb_write_io_threads = 8
|
||||
|
||||
table_open_cache = 4000
|
||||
tmp_table_size = 128M
|
||||
max_heap_table_size = 128M
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
@@ -0,0 +1,5 @@
|
||||
# Host profile: 1 vCPU / 4 GiB
|
||||
NEXUS_MEM_LIMIT=1g
|
||||
NEXUS_CPUS=1
|
||||
NEXUS_DB_POOL_SIZE=15
|
||||
NEXUS_DB_MAX_OVERFLOW=10
|
||||
@@ -0,0 +1,5 @@
|
||||
# Host profile: 2 vCPU / 8 GiB — default production
|
||||
NEXUS_MEM_LIMIT=2g
|
||||
NEXUS_CPUS=1.5
|
||||
NEXUS_DB_POOL_SIZE=30
|
||||
NEXUS_DB_MAX_OVERFLOW=20
|
||||
@@ -0,0 +1,5 @@
|
||||
# Host profile: 4 vCPU / 16 GiB
|
||||
NEXUS_MEM_LIMIT=4g
|
||||
NEXUS_CPUS=3
|
||||
NEXUS_DB_POOL_SIZE=50
|
||||
NEXUS_DB_MAX_OVERFLOW=30
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write project-root .env from docker/.env for host uvicorn (127.0.0.1 MySQL/Redis)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from server.utils.text_io import read_utf8_lf, write_utf8_lf
|
||||
DOCKER_ENV = ROOT / "docker" / ".env"
|
||||
OUT = ROOT / ".env"
|
||||
|
||||
|
||||
def _parse(path: Path) -> dict[str, str]:
|
||||
data: dict[str, str] = {}
|
||||
for line in read_utf8_lf(path).splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
data[key.strip()] = value.strip()
|
||||
return data
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not DOCKER_ENV.exists():
|
||||
raise SystemExit(f"Missing {DOCKER_ENV} — run: python3 docker/generate_env.py")
|
||||
|
||||
d = _parse(DOCKER_ENV)
|
||||
mysql_pass = d.get("MYSQL_PASSWORD", "")
|
||||
if not mysql_pass:
|
||||
raise SystemExit("MYSQL_PASSWORD missing in docker/.env")
|
||||
|
||||
for key in ("NEXUS_SECRET_KEY", "NEXUS_API_KEY", "NEXUS_ENCRYPTION_KEY"):
|
||||
if not d.get(key):
|
||||
raise SystemExit(f"{key} missing in docker/.env — run generate_env.py")
|
||||
|
||||
db_user = "nexus"
|
||||
db_pass_q = quote_plus(mysql_pass)
|
||||
deploy = str(ROOT)
|
||||
cors = d.get("NEXUS_CORS_ORIGINS", "http://localhost:8600,http://127.0.0.1:8600")
|
||||
api_base = d.get("NEXUS_API_BASE_URL", "http://localhost:8600")
|
||||
|
||||
lines = [
|
||||
"# Nexus .env — host uvicorn dev (from docker/sync_root_env.py)",
|
||||
"",
|
||||
"NEXUS_HOST=0.0.0.0",
|
||||
"NEXUS_PORT=8600",
|
||||
f"NEXUS_DEPLOY_PATH={deploy}",
|
||||
f"NEXUS_CORS_ORIGINS={cors}",
|
||||
f"NEXUS_API_BASE_URL={api_base}",
|
||||
"",
|
||||
f"NEXUS_DATABASE_URL=mysql+aiomysql://{db_user}:{db_pass_q}@127.0.0.1:3306/nexus",
|
||||
"NEXUS_DB_POOL_SIZE=20",
|
||||
"NEXUS_DB_MAX_OVERFLOW=10",
|
||||
"",
|
||||
f"NEXUS_SECRET_KEY={d['NEXUS_SECRET_KEY']}",
|
||||
f"NEXUS_API_KEY={d['NEXUS_API_KEY']}",
|
||||
f"NEXUS_ENCRYPTION_KEY={d['NEXUS_ENCRYPTION_KEY']}",
|
||||
"",
|
||||
"NEXUS_REDIS_URL=redis://127.0.0.1:6379/0",
|
||||
"NEXUS_SSH_STRICT_HOST_CHECKING=false",
|
||||
"",
|
||||
"# MySQL MCP (optional): python3 scripts/sync_mysql_mcp_env.py",
|
||||
"MYSQL_HOST=127.0.0.1",
|
||||
"MYSQL_PORT=3306",
|
||||
f"MYSQL_USER={db_user}",
|
||||
f"MYSQL_PASSWORD={mysql_pass}",
|
||||
"MYSQL_DATABASE=nexus",
|
||||
"MYSQL_READONLY=false",
|
||||
"MYSQL_DATABASE_ALLOWLIST=nexus",
|
||||
]
|
||||
|
||||
write_utf8_lf(OUT, "\n".join(lines) + "\n")
|
||||
OUT.chmod(0o600)
|
||||
print(f"Wrote {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,733 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Nexus 6.0 — 功能说明书</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0f172a;color:#e2e8f0;line-height:1.6}
|
||||
a{color:#60a5fa;text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
|
||||
/* Layout */
|
||||
.wrap{max-width:960px;margin:0 auto;padding:32px 24px 80px}
|
||||
|
||||
/* Header */
|
||||
.hero{text-align:center;padding:48px 0 40px;border-bottom:1px solid #1e293b;margin-bottom:40px}
|
||||
.hero h1{font-size:2.2rem;font-weight:700;color:#f8fafc;letter-spacing:-0.02em}
|
||||
.hero .sub{font-size:1rem;color:#64748b;margin-top:8px}
|
||||
.badge{display:inline-block;margin:4px;padding:3px 10px;border-radius:999px;font-size:0.72rem;font-weight:600}
|
||||
.badge-blue{background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb55}
|
||||
.badge-green{background:#14371f;color:#4ade80;border:1px solid #16a34a55}
|
||||
.badge-amber{background:#3d2a00;color:#fbbf24;border:1px solid #d9770655}
|
||||
.badge-red{background:#3b1212;color:#f87171;border:1px solid #dc262655}
|
||||
|
||||
/* TOC */
|
||||
.toc{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:20px 24px;margin-bottom:36px}
|
||||
.toc h2{font-size:0.85rem;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px}
|
||||
.toc ol{padding-left:18px;column-count:2;column-gap:24px}
|
||||
.toc li{margin:5px 0;font-size:0.88rem}
|
||||
|
||||
/* Section */
|
||||
.section{margin-bottom:48px}
|
||||
.section-title{font-size:1.4rem;font-weight:700;color:#f1f5f9;padding-bottom:10px;border-bottom:2px solid #334155;margin-bottom:24px;display:flex;align-items:center;gap:10px}
|
||||
.section-title .icon{font-size:1.2rem}
|
||||
|
||||
/* Card */
|
||||
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;margin-bottom:16px}
|
||||
.card-title{font-size:1.05rem;font-weight:600;color:#f1f5f9;margin-bottom:6px;display:flex;align-items:center}
|
||||
.card-title .num{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:#334155;font-size:0.7rem;font-weight:700;color:#94a3b8;margin-right:8px;shrink:0}
|
||||
.card-desc{color:#94a3b8;font-size:0.88rem;margin-bottom:14px}
|
||||
|
||||
/* Info rows */
|
||||
.info-grid{display:grid;grid-template-columns:1fr;gap:10px}
|
||||
.info-block{border-radius:8px;padding:12px 14px}
|
||||
.info-block.usage{background:#0f2d1f;border:1px solid #166534}
|
||||
.info-block.warn{background:#2a1a00;border:1px solid #92400e}
|
||||
.info-block.note{background:#1a1f2e;border:1px solid #4338ca}
|
||||
.info-title{font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
|
||||
.info-title.u{color:#4ade80}
|
||||
.info-title.w{color:#fbbf24}
|
||||
.info-title.n{color:#818cf8}
|
||||
.info-block ul{padding-left:16px;font-size:0.86rem;color:#cbd5e1}
|
||||
.info-block li{margin:4px 0}
|
||||
.info-block p{font-size:0.86rem;color:#cbd5e1}
|
||||
|
||||
/* Sub table */
|
||||
.feat-table{width:100%;border-collapse:collapse;font-size:0.86rem;margin-top:10px}
|
||||
.feat-table th{text-align:left;color:#64748b;font-weight:600;font-size:0.75rem;text-transform:uppercase;padding:6px 10px;border-bottom:1px solid #334155}
|
||||
.feat-table td{padding:7px 10px;border-bottom:1px solid #1e293b;color:#cbd5e1;vertical-align:top}
|
||||
.feat-table tr:last-child td{border-bottom:none}
|
||||
|
||||
/* Footer */
|
||||
footer{text-align:center;color:#334155;font-size:0.8rem;margin-top:60px;padding-top:24px;border-top:1px solid #1e293b}
|
||||
code{background:#334155;color:#7dd3fc;padding:2px 6px;border-radius:4px;font-size:0.83em;font-family:monospace}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<!-- HERO -->
|
||||
<div class="hero">
|
||||
<h1>⚡ Nexus 6.0</h1>
|
||||
<p class="sub">服务器运维管理平台 — 全站功能说明书 · 2026-05-23</p>
|
||||
<p class="sub" style="margin-top:10px;font-size:0.85rem">
|
||||
Markdown 维护版:<code>docs/project/nexus-full-site-features.md</code> ·
|
||||
AI 接续:<code>docs/project/AI-HANDOFF-2026-05-23.md</code>
|
||||
</p>
|
||||
<div style="margin-top:14px">
|
||||
<span class="badge badge-blue">FastAPI</span>
|
||||
<span class="badge badge-blue">SQLAlchemy 2.0</span>
|
||||
<span class="badge badge-blue">Redis 7</span>
|
||||
<span class="badge badge-blue">asyncssh</span>
|
||||
<span class="badge badge-blue">Tailwind v4</span>
|
||||
<span class="badge badge-green">2000+ 服务器</span>
|
||||
<span class="badge badge-green">JWT + TOTP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOC -->
|
||||
<div class="toc">
|
||||
<h2>目录</h2>
|
||||
<ol>
|
||||
<li><a href="#sitemap">全站页面地图</a></li>
|
||||
<li><a href="#network">网络与通道</a></li>
|
||||
<li><a href="#auth">认证与访问控制</a></li>
|
||||
<li><a href="#dashboard">仪表盘</a></li>
|
||||
<li><a href="#alerts">告警中心</a></li>
|
||||
<li><a href="#servers">服务器管理</a></li>
|
||||
<li><a href="#assets">资产管理</a></li>
|
||||
<li><a href="#webssh">Web SSH</a></li>
|
||||
<li><a href="#commands">命令与推送日志</a></li>
|
||||
<li><a href="#push">批量文件推送</a></li>
|
||||
<li><a href="#scripts">脚本与执行历史</a></li>
|
||||
<li><a href="#files">远程文件管理</a></li>
|
||||
<li><a href="#schedules">定时调度</a></li>
|
||||
<li><a href="#credentials">凭据管理</a></li>
|
||||
<li><a href="#retries">重试队列</a></li>
|
||||
<li><a href="#search">全局搜索</a></li>
|
||||
<li><a href="#settings">系统设置</a></li>
|
||||
<li><a href="#audit">审计日志</a></li>
|
||||
<li><a href="#install">安装向导</a></li>
|
||||
<li><a href="#guardian">3 层守护</a></li>
|
||||
<li><a href="#agent">子机 Agent</a></li>
|
||||
<li><a href="#background">后台任务</a></li>
|
||||
<li><a href="#acceptance">开发验收标准</a></li>
|
||||
<li><a href="#notimpl">未实现 / 已否决</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- 0. SITEMAP -->
|
||||
<div class="section" id="sitemap">
|
||||
<div class="section-title"><span class="icon">🗺</span> 全站页面地图(18 页)</div>
|
||||
<div class="card">
|
||||
<table class="feat-table">
|
||||
<tr><th>页面</th><th>路径</th><th>功能</th></tr>
|
||||
<tr><td>安装向导</td><td><code>/app/install.html</code></td><td>无 .env 时安装中心机</td></tr>
|
||||
<tr><td>登录</td><td><code>/app/login.html</code></td><td>JWT + TOTP + 防暴破</td></tr>
|
||||
<tr><td>仪表盘</td><td><code>/app/index.html</code></td><td>统计、WS 刷新统计</td></tr>
|
||||
<tr><td>服务器</td><td><code>/app/servers.html</code></td><td>CRUD、Agent 安装/升级、漂移徽章</td></tr>
|
||||
<tr><td>资产管理</td><td><code>/app/assets.html</code></td><td>Platform / Node</td></tr>
|
||||
<tr><td>文件管理</td><td><code>/app/files.html</code></td><td>SSH 浏览与文件操作</td></tr>
|
||||
<tr><td>推送</td><td><code>/app/push.html</code></td><td>rsync 批量 + dry-run</td></tr>
|
||||
<tr><td>脚本库</td><td><code>/app/scripts.html</code></td><td>脚本 CRUD、直接执行</td></tr>
|
||||
<tr><td>执行历史</td><td><code>/app/script-executions.html</code></td><td>停止/重试/日志</td></tr>
|
||||
<tr><td>凭据</td><td><code>/app/credentials.html</code></td><td>DB 凭据 + 密码预设</td></tr>
|
||||
<tr><td>定时调度</td><td><code>/app/schedules.html</code></td><td>Cron 推送或脚本</td></tr>
|
||||
<tr><td>重试队列</td><td><code>/app/retries.html</code></td><td>推送失败重试</td></tr>
|
||||
<tr><td>命令日志</td><td><code>/app/commands.html</code></td><td>WebSSH 命令 + 推送日志</td></tr>
|
||||
<tr><td>告警中心</td><td><code>/app/alerts.html</code></td><td>告警历史与统计</td></tr>
|
||||
<tr><td>审计</td><td><code>/app/audit.html</code></td><td>操作审计、多维过滤</td></tr>
|
||||
<tr><td>设置</td><td><code>/app/settings.html</code></td><td>阈值、Telegram、白名单、TOTP</td></tr>
|
||||
<tr><td>WebSSH</td><td><code>/app/terminal.html</code></td><td>xterm 终端</td></tr>
|
||||
</table>
|
||||
<p class="card-desc" style="margin-top:12px">共享:<code>layout.js</code>(侧栏/移动适配)、<code>api.js</code>(JWT、toast、全局搜索)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NETWORK -->
|
||||
<div class="section" id="network">
|
||||
<div class="section-title"><span class="icon">🔌</span> 网络与通道模型</div>
|
||||
<div class="card">
|
||||
<table class="feat-table">
|
||||
<tr><th>方向</th><th>端口</th><th>用途</th><th>必须</th></tr>
|
||||
<tr><td>子机 → 中心</td><td><strong>443 出站</strong></td><td>Agent 心跳、长任务 curl 回调</td><td>✅</td></tr>
|
||||
<tr><td>中心 → 子机</td><td><strong>22 入站</strong></td><td>推送、文件、脚本、健康检查、Agent 安装</td><td>✅</td></tr>
|
||||
<tr><td>子机本机</td><td><code>127.0.0.1:8601</code></td><td>Agent HTTP(可不暴露公网)</td><td>可选</td></tr>
|
||||
</table>
|
||||
<div class="info-block note" style="margin-top:12px"><div class="info-title n">说明</div><p>脚本执行与手动健康检查已改走 <strong>SSH</strong>,不依赖公网 8601。宝塔环境请用系统 Python 3.12 + <code>install.sh</code> venv,勿用面板 Python 项目管理器托管 Agent。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. AUTH -->
|
||||
<div class="section" id="auth">
|
||||
<div class="section-title"><span class="icon">🔒</span> 1. 认证与访问控制</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> JWT 登录 + 自动刷新</div>
|
||||
<p class="card-desc">管理员通过用户名+密码登录,获取 Access Token(30 分钟)和 Refresh Token(7 天)。前端到期前 2 分钟自动换新 Token,无感续期。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>访问 <code>/app/login.html</code>,输入管理员账号密码</li>
|
||||
<li>Token 存于 <code>localStorage</code>,关闭浏览器后 7 天内免登录</li>
|
||||
<li>8 小时无操作自动退出</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>JWT 存于 localStorage,XSS 漏洞可能导致 Token 泄露(已做 CSP 缓解)</li>
|
||||
<li>无 RBAC:只有一个管理员角色,鉴权即信任</li>
|
||||
<li>不支持多管理员角色区分</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> TOTP 双因素认证</div>
|
||||
<p class="card-desc">可选启用 Google Authenticator 兼容的 TOTP。登录时先验密码,再验 6 位动态码(30 秒窗口)。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>进入「设置」→「安全设置」→ 启用 / <strong>重新绑定</strong> TOTP,扫描二维码</li>
|
||||
<li>禁用 TOTP 需同时提供当前密码 + TOTP 码</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>TOTP 秘钥丢失无法恢复,需提前备份</li>
|
||||
<li>系统时间偏差 >30s 会导致验证失败(接受 ±1 窗口共 90s)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> 登录防暴破</div>
|
||||
<p class="card-desc">同一 IP + 用户名 15 分钟内失败 5 次,账号自动锁定并返回 HTTP 429。</p>
|
||||
<div class="info-block note"><div class="info-title n">注意</div><p>锁定基于 IP + 用户名组合,更换 IP 可绕过。适合防自动化爆破,不防分布式攻击。</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
|
||||
<p class="card-desc">可在设置中开启「仅允许白名单 IP 登录」。支持手动 IP/CIDR,以及从<strong>代理订阅 URL</strong>解析节点 IP(每 2 小时自动刷新,与手动列表合并)。</p>
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「设置」→「登录 IP 白名单」→ 总开关 + 订阅地址 + 手动 IP</li>
|
||||
<li>非白名单 IP 登录被拒绝并记审计</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. DASHBOARD -->
|
||||
<div class="section" id="dashboard">
|
||||
<div class="section-title"><span class="icon">📊</span> 2. 仪表盘与实时告警</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 实时统计面板</div>
|
||||
<p class="card-desc">首页展示服务器总数、在线数、离线数、告警中台数、最近推送记录。数据来源 Redis(实时)+ MySQL(历史),通过 WebSocket 触发刷新。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>在线状态取自 Redis <code>heartbeat:{id}</code>,子机 60s 上报一次</li>
|
||||
<li>告警计数 = Redis <code>alerts:*</code> key 数量</li>
|
||||
<li>WS 断线会自动重连(3 次指数退避)</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>在线状态最多滞后 60s(子机心跳间隔)</li>
|
||||
<li>Redis 不可用时统计退化为 MySQL 数据,可能不准</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 告警通知(Telegram)</div>
|
||||
<p class="card-desc">CPU / 内存 / 磁盘超过阈值(默认 80%)时,通过 Telegram Bot 推送告警;恢复后发送恢复通知。同服务器同指标 5 分钟内不重复推送。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「设置」配置 <code>TELEGRAM_BOT_TOKEN</code> + <code>TELEGRAM_CHAT_ID</code></li>
|
||||
<li>阈值可在设置页调整(CPU/内存/磁盘各独立)</li>
|
||||
<li>浏览器仪表盘<strong>只显示计数,不弹窗、不响铃</strong>(设计决策)</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>浏览器仪表盘不弹告警列表;历史见「告警中心」页</li>
|
||||
<li>Telegram Bot 被封或网络不通时告警静默失败(有日志)</li>
|
||||
<li>告警防抖 key 存于进程内存,重启后防抖状态重置</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ALERTS -->
|
||||
<div class="section" id="alerts">
|
||||
<div class="section-title"><span class="icon">🔔</span> 告警中心</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 告警历史(alerts.html)</div>
|
||||
<p class="card-desc">将 CPU/内存/磁盘告警与恢复事件持久化到 <code>alert_logs</code>,支持分页、按服务器/类型/日期筛选,以及近 7 日统计图表数据。</p>
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>侧栏「告警中心」查看历史列表</li>
|
||||
<li>API:<code>GET /api/alert-history/</code>、<code>GET /api/alert-history/stats</code></li>
|
||||
</ul></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 分项 Telegram 开关</div>
|
||||
<p class="card-desc">设置页可单独关闭某类告警的 Telegram 推送(告警/恢复/系统/时钟漂移等),便于后续扩展通道。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. SERVERS -->
|
||||
<div class="section" id="servers">
|
||||
<div class="section-title"><span class="icon">🖥</span> 3. 服务器管理</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 服务器 CRUD</div>
|
||||
<p class="card-desc">增删改查服务器配置,支持 SSH 密钥 / 密码双认证,可按平台类型(Platform)和节点(Node 树)分组。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>新增服务器:填写名称、域名/IP、SSH 端口、认证方式</li>
|
||||
<li>认证方式选「密钥」时粘贴私钥内容,自动 Fernet 加密存储</li>
|
||||
<li>支持 <code>platform_id</code> / <code>node_id</code> 分组归类</li>
|
||||
<li>服务器列表分页显示(默认 50 条/页)</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>批量导入(CSV)未实现,2000 台需逐一或通过 API 批量添加</li>
|
||||
<li>删除服务器同时删除关联推送日志(CASCADE)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 健康检查</div>
|
||||
<p class="card-desc">手动触发 <code>POST /api/servers/check</code>:经 <strong>SSH</strong> 执行 Python 采集 CPU/内存/磁盘(<code>remote_probe</code>),<strong>不需要</strong>公网 8601。</p>
|
||||
<div class="info-block note"><div class="info-title n">与在线状态区别</div><p>列表「在线」主要来自 Agent <strong>443 心跳</strong>;健康检查用于即时 SSH 探活与指标快照。</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">4</span> Agent 安装与升级</div>
|
||||
<p class="card-desc">服务器详情「Agent 安装」:复制 <code>install.sh</code> 命令或一键 SSH 安装(带日志);安装后轮询在线。升级通过 SSH 拉取最新 <code>agent.py</code> 并 restart。</p>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>安装脚本:Python 3.12 venv、监听 <code>127.0.0.1:8601</code>,无需公网放行 8601</li>
|
||||
<li>时钟漂移:心跳带 <code>agent_time</code>,列表显示 warn/crit 徽章</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> 每服务器 API Key</div>
|
||||
<p class="card-desc">为每台服务器单独生成 Agent API Key(格式 <code>nxs-xxx</code>),用于该服务器子机与中心通信的专属认证。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>服务器详情 → 「生成 Agent Key」,完整 Key 仅本次显示,立即复制到子机 <code>config.json</code></li>
|
||||
<li>列表页只显示前 8 位用于识别</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>Key 生成后无法再次查看(一次性展示设计)</li>
|
||||
<li>丢失需重新生成并更新子机配置</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ASSETS -->
|
||||
<div class="section" id="assets">
|
||||
<div class="section-title"><span class="icon">🗂</span> 资产管理</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> Platform 与 Node</div>
|
||||
<p class="card-desc"><code>assets.html</code> 管理资产类型模板(Platform)与树形节点(Node),服务器表单可关联分组。</p>
|
||||
<p class="card-desc">API:<code>/api/assets/platforms</code>、<code>/api/assets/nodes</code>(含 tree)</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> SSH 会话与命令(只读查询)</div>
|
||||
<p class="card-desc">同页或「命令日志」可查询 <code>ssh_sessions</code>、<code>command_logs</code>(WebSSH 产生)。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. WEB SSH -->
|
||||
<div class="section" id="webssh">
|
||||
<div class="section-title"><span class="icon">💻</span> 4. Web SSH 终端</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 浏览器终端</div>
|
||||
<p class="card-desc">基于 xterm.js + asyncssh,在浏览器内打开远程服务器 SSH 会话,支持全屏、自动 resize、彩色输出。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>服务器列表点击「终端」→ 弹出 <code>/app/terminal.html?server_id=X</code></li>
|
||||
<li>认证:先获取短效 webssh-token(15 分钟),绑定到该服务器,再建 WebSocket</li>
|
||||
<li>支持窗口 resize 自动通知远程 shell</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>Token 通过 URL 参数传递(?token=...),HTTPS 下安全;HTTP 下有风险</li>
|
||||
<li>不支持文件上传 / 拖拽(需用文件管理页)</li>
|
||||
<li>会话不录像(已明确否决)</li>
|
||||
<li>网络中断 WebSocket 不会自动重连,需刷新页面</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- COMMANDS -->
|
||||
<div class="section" id="commands">
|
||||
<div class="section-title"><span class="icon">💻</span> 命令与推送日志</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> WebSSH 会话与命令</div>
|
||||
<p class="card-desc"><code>commands.html</code>:会话列表、命令列表(危险命令高亮)、按服务器过滤。数据来自 WebSSH,直接 SSH 不记录。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 推送日志</div>
|
||||
<p class="card-desc">同页「推送日志」Tab:rsync 批量推送历史,支持服务器/状态/同步模式/触发方式过滤与分页。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. PUSH -->
|
||||
<div class="section" id="push">
|
||||
<div class="section-title"><span class="icon">📤</span> 5. 批量文件推送</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> rsync 批量推送</div>
|
||||
<p class="card-desc">将源目录通过 rsync-over-SSH 推送到选定的服务器列表,支持 4 种同步模式,内置分批(50 台/批)并发(10 路)控制。</p>
|
||||
<table class="feat-table">
|
||||
<tr><th>同步模式</th><th>rsync 参数</th><th>适用场景</th></tr>
|
||||
<tr><td>增量同步</td><td><code>-az</code></td><td>日常更新,只同步有变动的文件</td></tr>
|
||||
<tr><td>全量同步</td><td><code>-az --delete</code></td><td>强制目标与源完全一致,会删除目标多余文件</td></tr>
|
||||
<tr><td>校验和</td><td><code>-az --checksum</code></td><td>按文件内容而非时间戳判断差异</td></tr>
|
||||
<tr><td>直接覆盖</td><td><code>-az --inplace</code></td><td>不用临时文件,直接覆盖(大文件节省空间)</td></tr>
|
||||
</table>
|
||||
<div class="info-grid" style="margin-top:12px">
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li><strong>全量同步(--delete)会不可逆删除</strong>目标目录多余文件,务必先「预览」</li>
|
||||
<li>rsync 在中心机运行,中心机需能 SSH 到所有子机</li>
|
||||
<li>失败的服务器自动加入重试队列(指数退避,最多 100 次)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 推送前预览(dry-run)</div>
|
||||
<p class="card-desc">选中全量/覆盖模式时,页面显示橙色警告,引导先对首台服务器运行 <code>rsync --dry-run --stats</code>,查看待传输/删除文件数量后再决定是否推送。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>选择「全量同步」→ 出现橙色预览按钮 → 点击 → 展示首台服务器变动摘要</li>
|
||||
<li>可展开「详细文件列表」(最多 200 行)</li>
|
||||
<li>预览不阻塞推送,看完数据自行决定是否点「开始推送」</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>仅预览第一台选中的服务器,其余服务器可能略有差异</li>
|
||||
<li>预览需 SSH 连通,与真实推送消耗相同时间(通常 <5s)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> 失败自动重试队列</div>
|
||||
<p class="card-desc">推送失败的服务器自动进入 <code>push_retry_jobs</code> 队列,后台每 5 分钟按指数退避重试(60s → 120s → 240s...,最大 64 分钟)。</p>
|
||||
<div class="info-block note"><div class="info-title n">注意</div><p>最多重试 100 次。可在「重试」页手动强制立即重试或删除任务。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. SCRIPTS -->
|
||||
<div class="section" id="scripts">
|
||||
<div class="section-title"><span class="icon">📜</span> 脚本与执行历史</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 脚本库管理</div>
|
||||
<p class="card-desc">维护可复用 Shell 脚本库(按 ops/deploy/check/cleanup 分类),一键下发到选定的服务器。</p>
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「脚本」页 → 新建脚本 → 填写名称/类别/内容</li>
|
||||
<li>点击脚本名 → 编辑;点击「执行」→ 选择服务器 → 运行</li>
|
||||
<li>支持直接输入临时命令(无需保存到库)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 批量执行(短任务 / 长任务)</div>
|
||||
<p class="card-desc">短任务:同步等待结果(默认 30s 超时)。长任务:nohup 后台运行,完成后 curl 回调中心记录结果。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>选择服务器 → 填写命令 → 「执行」</li>
|
||||
<li>执行走 <strong>SSH 22</strong>(结果含 <code>channel: ssh</code>),无需子机公网 8601</li>
|
||||
<li>长任务:nohup + 子机 curl 回调中心(需 HTTPS <code>NEXUS_API_BASE_URL</code>)</li>
|
||||
<li>「执行历史」页:停止 / 重试 / 标记卡住 / 拉取 nexus-job 日志</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li><code>rm -rf /</code> 等危险命令被中心<strong>拦截(400)</strong>,但仅匹配预设规则</li>
|
||||
<li>长任务依赖子机能访问中心 <code>NEXUS_API_BASE_URL</code>(必须配置且使用 HTTPS)</li>
|
||||
<li>执行历史默认保留 2 小时 Redis 热数据,60s 刷入 MySQL</li>
|
||||
<li>执行命令时密码等敏感 $DB_* 变量存储时已脱敏(替换为 ***)</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> $DB_* 数据库凭据替换</div>
|
||||
<p class="card-desc">脚本中使用 <code>$DB_USER</code> / <code>$DB_PASS</code> / <code>$DB_HOST</code> / <code>$DB_PORT</code> / <code>$DB_NAME</code> 占位符,执行时自动替换为选定的数据库凭据,密码不出现在脚本文本或日志中。</p>
|
||||
<div class="info-block note"><div class="info-title n">注意</div><p>替换在中心机完成后发往子机,子机接收到的已是明文命令;传输通过 HTTPS,子机侧无加密保护。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 7. FILES -->
|
||||
<div class="section" id="files">
|
||||
<div class="section-title"><span class="icon">📁</span> 7. 远程文件管理</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 目录浏览</div>
|
||||
<p class="card-desc">通过 SSH exec(<code>ls -la</code>)浏览指定服务器的远程目录,支持面包屑导航和目录点击进入。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「文件管理」→ 选服务器 → 输入路径 → 「浏览」(或按 Enter)</li>
|
||||
<li>点击目录名进入子目录,点击「..」返回上级</li>
|
||||
<li>文件按 目录优先 / 字母顺序 排列</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>无左侧树状面板(每次需手动导航)</li>
|
||||
<li>显示的文件大小为 <code>ls</code> 输出的字节数,大文件不友好</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 文件操作(删除/重命名/新建目录)</div>
|
||||
<p class="card-desc">鼠标悬停文件/目录行显示操作按钮,通过 SSH exec 在远程执行操作。</p>
|
||||
<table class="feat-table">
|
||||
<tr><th>操作</th><th>命令</th><th>触发方式</th></tr>
|
||||
<tr><td>删除文件</td><td><code>rm -f path</code></td><td>悬停 → 🗑 → confirm 弹窗</td></tr>
|
||||
<tr><td>删除目录</td><td><code>rm -rf path</code></td><td>悬停 → 🗑 → 额外警告弹窗</td></tr>
|
||||
<tr><td>重命名</td><td><code>mv src dst</code></td><td>悬停 → ✏ → prompt 输入新名</td></tr>
|
||||
<tr><td>新建目录</td><td><code>mkdir -p path</code></td><td>右上角 📁+ → prompt 输入名</td></tr>
|
||||
</table>
|
||||
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>删除目录(rm -rf)<strong>不可恢复</strong>,浏览器弹窗确认无法替代真正的安全保障</li>
|
||||
<li>无文件编辑器(已明确否决,用 WebSSH vim/nano 代替)</li>
|
||||
<li>无文件上传功能(可用 SFTP API 或 WebSSH scp)</li>
|
||||
<li>无文件内容预览</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 8. SCHEDULES -->
|
||||
<div class="section" id="schedules">
|
||||
<div class="section-title"><span class="icon">⏰</span> 8. 定时调度</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> Cron 调度(推送 + 脚本)</div>
|
||||
<p class="card-desc">支持两种 <code>schedule_type</code>:<strong>文件推送</strong> 或 <strong>脚本执行</strong>。5 字段 Cron,后台每 60s 检查(仅 Primary Worker)。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「定时」→ 选类型 → 填 Cron、目标服务器</li>
|
||||
<li>脚本型:选脚本库 ID 或内联内容,可设超时与长任务</li>
|
||||
<li>示例:<code>0 2 * * *</code> = 每天凌晨 2 点</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>Cron 精度受 60s 检查间隔限制</li>
|
||||
<li>脚本调度同样走 SSH 执行通道</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RETRIES -->
|
||||
<div class="section" id="retries">
|
||||
<div class="section-title"><span class="icon">🔄</span> 重试队列</div>
|
||||
<div class="card">
|
||||
<p class="card-desc"><code>retries.html</code> 管理推送失败任务:<code>retry_runner</code> 每 5 分钟指数退避;可手动立即重试或删除。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEARCH -->
|
||||
<div class="section" id="search">
|
||||
<div class="section-title"><span class="icon">🔍</span> 全局搜索</div>
|
||||
<div class="card">
|
||||
<p class="card-desc">侧栏搜索框调用 <code>GET /api/search/</code>,跨服务器、脚本、凭据、定时任务关键字匹配。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 9. CREDENTIALS -->
|
||||
<div class="section" id="credentials">
|
||||
<div class="section-title"><span class="icon">🔑</span> 9. 凭据管理</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 数据库凭据</div>
|
||||
<p class="card-desc">存储 MySQL/PostgreSQL 连接信息,密码 Fernet 加密,API 响应不含密码(仅元数据)。供脚本执行时通过 $DB_* 变量引用。</p>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><p>凭据只加密存储,子机接收到的命令已含明文连接信息(仅在中心→子机的 HTTPS 传输中受保护)。</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> 密码预设</div>
|
||||
<p class="card-desc">存储 SSH 密码模板(Fernet 加密),可在推送配置中引用,避免重复输入常用密码。</p>
|
||||
<div class="info-block note"><div class="info-title n">注意</div><p>密码预设不能用于脚本 $DB_PASS 替换,只用于推送密码模板。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 10. SETTINGS -->
|
||||
<div class="section" id="settings">
|
||||
<div class="section-title"><span class="icon">⚙️</span> 10. 系统设置</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 可配置项</div>
|
||||
<table class="feat-table">
|
||||
<tr><th>设置项</th><th>说明</th><th>是否可改</th></tr>
|
||||
<tr><td>系统名称 / 标题</td><td>品牌展示名称</td><td>✅ 可改</td></tr>
|
||||
<tr><td>告警阈值(CPU/内存/磁盘)</td><td>超过阈值触发 Telegram 告警,默认 80%</td><td>✅ 可改</td></tr>
|
||||
<tr><td>Redis URL</td><td>Redis 连接地址</td><td>✅ 可改</td></tr>
|
||||
<tr><td>数据库连接池大小</td><td>pool_size / max_overflow</td><td>✅ 可改</td></tr>
|
||||
<tr><td>Telegram Bot Token / Chat ID</td><td>告警推送配置</td><td>✅ 可改</td></tr>
|
||||
<tr><td>SECRET_KEY / API_KEY / ENCRYPTION_KEY</td><td>核心安全密钥</td><td>❌ 不可改(需改 .env 重启)</td></tr>
|
||||
<tr><td>DATABASE_URL</td><td>数据库连接</td><td>❌ 不可改</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">2</span> Telegram</div>
|
||||
<p class="card-desc">配置 Bot Token / Chat ID;<strong>测试发送</strong>;通过 getUpdates <strong>检测 Chat ID</strong>;各告警类型独立开关。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">3</span> TOTP 与 API Key</div>
|
||||
<p class="card-desc">TOTP 启用/禁用/<strong>重新绑定</strong>;全局 API Key 需密码二次验证后查看(记审计)。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
|
||||
<p class="card-desc">总开关、订阅 URL(2h 刷新)、手动 IP、解析预览(见「认证」章节)。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 11. AUDIT -->
|
||||
<div class="section" id="audit">
|
||||
<div class="section-title"><span class="icon">📋</span> 11. 审计日志</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 操作审计</div>
|
||||
<p class="card-desc">所有 CUD 操作(创建/更新/删除服务器、推送、执行脚本、登录、改密码等)自动写入 <code>audit_logs</code>,记录操作人、IP、目标、详情。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>「审计」页:日期起止、管理员用户名、操作类型(分组下拉)</li>
|
||||
<li>每页 50 条分页</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||
<li>无日志导出(已否决 CSV/JSON)</li>
|
||||
<li>审计日志不会自动清理,长期需归档</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 12. INSTALL -->
|
||||
<div class="section" id="install">
|
||||
<div class="section-title"><span class="icon">🚀</span> 12. 安装向导</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 5 步安装向导</div>
|
||||
<p class="card-desc">无 <code>.env</code> 文件时系统进入安装模式,访问 <code>/app/install.html</code> 完成 5 步配置:欢迎 → 数据库 → Redis → 管理员账号 → 完成。自动生成 API Key / Secret Key / Encryption Key。</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||
<li>安装完成后 <code>.env</code> 自动生成,安装 API 除 GET /status 外全部锁定(返回 403)</li>
|
||||
<li>重装需手动删除 <code>.env</code> 并重启</li>
|
||||
</ul></div>
|
||||
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||
<li>安装过程未验证数据库/Redis 连接成功才允许进入下一步(应在步骤中手动验证)</li>
|
||||
<li>安装模式下除 install API 和静态文件外,其余 API 返回 503</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 13. GUARDIAN -->
|
||||
<div class="section" id="guardian">
|
||||
<div class="section-title"><span class="icon">🛡</span> 13. 三层守护机制</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> 三层守护</div>
|
||||
<table class="feat-table">
|
||||
<tr><th>层级</th><th>机制</th><th>触发条件</th><th>动作</th></tr>
|
||||
<tr><td>Layer 1</td><td>Supervisor</td><td>Python 进程崩溃</td><td>自动重启</td></tr>
|
||||
<tr><td>Layer 2</td><td>Python self_monitor(30s)</td><td>Redis/MySQL 不可达</td><td>发 Telegram 告警/恢复</td></tr>
|
||||
<tr><td>Layer 3</td><td>Shell cron(1 分钟)</td><td>连续 3 次 /health 失败</td><td>supervisorctl restart + Telegram</td></tr>
|
||||
</table>
|
||||
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">注意</div><p>Shell cron 依赖宝塔/系统 crontab 配置,部署时需手动添加定时任务。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 14. AGENT -->
|
||||
<div class="section" id="agent">
|
||||
<div class="section-title"><span class="icon">🤖</span> 14. 子机 Agent</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title"><span class="num">1</span> Agent 功能</div>
|
||||
<p class="card-desc">部署在每台被管服务器上的轻量服务:<strong>心跳走 443 出站</strong>;HTTP 仅本机 <code>127.0.0.1:8601</code>。中心侧脚本已改 <strong>SSH</strong>,/exec 端点保留供兼容。</p>
|
||||
<table class="feat-table">
|
||||
<tr><th>能力</th><th>说明</th></tr>
|
||||
<tr><td>心跳</td><td>POST 中心 <code>/api/agent/heartbeat</code>,写 Redis,智能加急</td></tr>
|
||||
<tr><td>时钟漂移</td><td><code>agent_time</code> 与中心比对,warn/crit + Telegram</td></tr>
|
||||
<tr><td>IP 白名单</td><td><code>allowed_ips</code> 限制访问本机 Agent HTTP</td></tr>
|
||||
<tr><td>长任务回调</td><td>POST <code>/api/agent/script-callback</code></td></tr>
|
||||
<tr><td>安装</td><td><code>web/agent/install.sh</code> — python3.12 venv + systemd</td></tr>
|
||||
</table>
|
||||
<div class="info-block warn" style="margin-top:12px"><div class="info-title w">注意</div><ul>
|
||||
<li>升级 API 存在,建议与 venv 安装脚本对齐后使用</li>
|
||||
<li>长任务回调需子机出站访问中心 HTTPS</li>
|
||||
</ul></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BACKGROUND -->
|
||||
<div class="section" id="background">
|
||||
<div class="section-title"><span class="icon">⚙</span> 后台任务(Primary Worker)</div>
|
||||
<div class="card">
|
||||
<table class="feat-table">
|
||||
<tr><th>任务</th><th>间隔</th><th>说明</th></tr>
|
||||
<tr><td>heartbeat_flush</td><td>10min</td><td>Redis 心跳 → MySQL</td></tr>
|
||||
<tr><td>self_monitor</td><td>30s</td><td>Redis/MySQL/WS 自检 + Telegram</td></tr>
|
||||
<tr><td>schedule_runner</td><td>60s</td><td>Cron 调度</td></tr>
|
||||
<tr><td>retry_runner</td><td>5min</td><td>推送重试</td></tr>
|
||||
<tr><td>ip_allowlist_refresh</td><td>2h</td><td>订阅 IP 刷新</td></tr>
|
||||
<tr><td>script_execution_flush</td><td>60s</td><td>执行记录 Redis→MySQL</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ACCEPTANCE -->
|
||||
<div class="section" id="acceptance">
|
||||
<div class="section-title"><span class="icon">✅</span> 标准与验收(与团队同等严谨)</div>
|
||||
<div class="card">
|
||||
<p class="card-desc"><strong>全量标准转接</strong>:<code>docs/project/standards-transfer-package.md</code> — 含 Cursor 规则、<code>standards/</code> 三件套、逐行八步 DoD、设计/上线清单。下一任 AI <strong>不得降低</strong>执行力度。</p>
|
||||
<table class="feat-table">
|
||||
<tr><th>类别</th><th>权威路径</th></tr>
|
||||
<tr><td>标准转接包</td><td><code>docs/project/standards-transfer-package.md</code></td></tr>
|
||||
<tr><td>验收 L0~L5</td><td><code>development-acceptance-standard.md</code></td></tr>
|
||||
<tr><td>系统开发</td><td><code>standards/system-development-standard.md</code></td></tr>
|
||||
<tr><td>逐行审计 v2</td><td><code>standards/line-walk-audit-standard-v2.md</code></td></tr>
|
||||
<tr><td>审计原则</td><td><code>standards/audit-core-principles.md</code></td></tr>
|
||||
<tr><td>Cursor</td><td><code>.cursor/rules/*.mdc</code>(含 audit-line-review)</td></tr>
|
||||
</table>
|
||||
<p class="card-desc" style="margin-top:12px">逐行审查:8 步/文件、Closure 全表含 SAFE、FINDING 必须 <code>文件:行号</code>、单回复 ≤5 文件。AI 接续:HANDOFF + 全站功能 + 标准转接包。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 15. NOT IMPL -->
|
||||
<div class="section" id="notimpl">
|
||||
<div class="section-title"><span class="icon">🚫</span> 15. 未实现 / 已否决功能</div>
|
||||
|
||||
<div class="card">
|
||||
<table class="feat-table">
|
||||
<tr><th>功能</th><th>状态</th><th>原因 / 替代方案</th></tr>
|
||||
<tr><td>文件编辑器(Monaco/ACE)</td><td><span class="badge badge-red">已否决</span></td><td>运维场景用 WebSSH + vim/nano;嵌入编辑器增加复杂度无实质收益</td></tr>
|
||||
<tr><td>会话录像(asciicast)</td><td><span class="badge badge-red">已否决</span></td><td>命令日志已满足审计需求</td></tr>
|
||||
<tr><td>RBAC 权限体系</td><td><span class="badge badge-red">已否决</span></td><td>鉴权即信任,单管理员模型;增加 RBAC 代价远超收益</td></tr>
|
||||
<tr><td>移动端适配</td><td><span class="badge badge-red">已否决</span></td><td>2000 台服务器运维不会用手机操作</td></tr>
|
||||
<tr><td>批量服务器导入(CSV)</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 API 批量 POST /api/servers/ 实现</td></tr>
|
||||
<tr><td>文件上传到远程服务器</td><td><span class="badge badge-amber">未实现</span></td><td>可用 WebSSH scp / 推送功能代替</td></tr>
|
||||
<tr><td>文件内容预览</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 WebSSH cat 查看</td></tr>
|
||||
<tr><td>WebSSH 断线自动重连</td><td><span class="badge badge-amber">未实现</span></td><td>当前需刷新页面;计划自动重试+按钮</td></tr>
|
||||
<tr><td>仪表盘历史趋势图</td><td><span class="badge badge-amber">未实现</span></td><td>仅实时 Redis 指标</td></tr>
|
||||
<tr><td>宝塔 API 监控集成</td><td><span class="badge badge-amber">未实现</span></td><td>可选;参考 btpanel-skills</td></tr>
|
||||
<tr><td>Agent mTLS</td><td><span class="badge badge-amber">讨论中</span></td><td>未实现</td></tr>
|
||||
<tr><td>首次部署 E2E(T1~T5、T-SE)</td><td><span class="badge badge-amber">待验证</span></td><td>见 production-verification-checklist.md</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<p>Nexus 6.0 全站功能说明书 · 2026-05-23(对齐代码库)</p>
|
||||
<p style="margin-top:6px">
|
||||
<a href="project/nexus-full-site-features.md">全站功能</a> ·
|
||||
<a href="project/standards-transfer-package.md">标准转接</a> ·
|
||||
<a href="project/development-acceptance-standard.md">验收标准</a> ·
|
||||
<a href="project/AI-HANDOFF-2026-05-23.md">AI 接续</a>
|
||||
</p>
|
||||
<p style="margin-top:4px">Python 3.12 / FastAPI 0.115 / MySQL 8.4 / Redis 7 / asyncssh 2.17 / Tailwind v4</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+75
-121
@@ -1,133 +1,87 @@
|
||||
# Nexus 文档索引
|
||||
|
||||
> 最后更新: 2026-05-23
|
||||
> 更新: 2026-06-04 · **唯一导航入口**
|
||||
|
||||
---
|
||||
|
||||
## project/ — 项目概况
|
||||
| 文件 | 说明 |
|
||||
## 1. 活跃 SSOT(日常必读)
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| `project/status.md` | 项目进度总览 + 数据库结构 + 实施进度 |
|
||||
| `project/roadmap.md` | 功能规划 + ADR架构决策(11项) + 技术债务 |
|
||||
| `project/deploy.md` | 部署指南 (Ubuntu 20-24, Nginx+Supervisor) |
|
||||
| **`project/phase-2-audit-remediation.md`** | **Phase 2 审计修复总览(开发 SSOT)** |
|
||||
| **`project/wsl-integration-test.md`** | **WSL 代码验证**(venv → 启动 → smoke,不审 key) |
|
||||
| `project/production-verification-checklist.md` | 首次部署验证清单(逐项勾选) |
|
||||
| `project/alert-push-policy.md` | 告警推送产品策略 + 数据流 |
|
||||
| `project/script-execution.md` | 脚本批量执行平台 SSOT |
|
||||
| `project/audit-line-review-cursor-rule.md` | 逐行走读 Cursor 规则 |
|
||||
| `project/line-walk-audit-standard.md` | 逐行走读判定标准 |
|
||||
| `project/mysql-mcp-setup.md` | Cursor MySQL MCP 安装与 `.env` 同步 |
|
||||
| `project/tech-stack-inventory.md` | 技术栈清单 + 连接池参数对齐 + v1勘误 |
|
||||
| [project/nexus-functional-development-guide.md](project/nexus-functional-development-guide.md) | 功能 / API / 14 页 / 部署 |
|
||||
| [project/nexus-functional-development-guide-appendix.md](project/nexus-functional-development-guide-appendix.md) | sync 端点、调用链、E2E 对照 |
|
||||
| [project/AI-HANDOFF-2026-06-03.md](project/AI-HANDOFF-2026-06-03.md) | **当前** 会话接续(复制提示词) |
|
||||
| [project/development-acceptance-standard.md](project/development-acceptance-standard.md) | 验收 L0–L5 |
|
||||
| [project/local-integration-test.md](project/local-integration-test.md) | `bash scripts/local_verify.sh` |
|
||||
| [project/linux-dev-paths.md](project/linux-dev-paths.md) | Linux 路径 |
|
||||
| [project/risk-acceptance-single-operator.md](project/risk-acceptance-single-operator.md) | 单人运维风险接受 |
|
||||
| [project/script-execution.md](project/script-execution.md) | 脚本执行 |
|
||||
| [project/alert-push-policy.md](project/alert-push-policy.md) | 告警策略 |
|
||||
| [project/deploy.md](project/deploy.md) | 部署补充(宝塔/裸机) |
|
||||
| [project/nexus-1panel-operations-knowledge.md](project/nexus-1panel-operations-knowledge.md) | **1Panel + Docker 运维知识 SSOT** |
|
||||
| [project/nexus-god-menu-audit-summary.md](project/nexus-god-menu-audit-summary.md) | **nx 上帝菜单巡检归档**(2026-06-06 round1–10) |
|
||||
| [project/production-verification-checklist.md](project/production-verification-checklist.md) | L5 清单 |
|
||||
|
||||
## design/ — 设计规格与实施计划
|
||||
根目录 [AGENTS.md](../AGENTS.md) · [CLAUDE.md](../CLAUDE.md) · [.cursorrules](../.cursorrules) 为薄入口。
|
||||
|
||||
### 活跃设计
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `design/specs/design-standards.md` | **设计标准** — 安全模型/性能基线/代码规范/架构模式 |
|
||||
| `design/specs/2026-05-18-redis-iot-optimization.md` | Redis+实时数据架构 + Key规范 |
|
||||
| `design/specs/2026-05-17-architecture-optimization.md` | 配置分发与部署架构 |
|
||||
| `design/specs/2026-05-19-sprint-planning.md` | Sprint规划 — 已完成升级项 + 待实现建议 |
|
||||
| `design/tech-stack-evaluation.md` | 技术栈评估 + 风险矩阵 + 连接池配置 |
|
||||
---
|
||||
|
||||
### 已归档设计(历史参考)
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `design/specs/2026-05-16-password-presets-design.md` | ⚠️ 归档 — 密码预设设计(引用旧文件结构) |
|
||||
| `design/specs/2026-05-16-simplified-push-design.md` | ⚠️ 归档 — 简化推送设计(引用旧文件结构) |
|
||||
| `design/specs/2026-05-17-retry-progress-design.md` | ⚠️ 归档 — 重试+进度设计(引用旧文件结构) |
|
||||
| `design/specs/2026-05-17-server-add-optimize-design.md` | ⚠️ 归档 — 服务器添加优化设计(引用旧文件结构) |
|
||||
| `design/specs/2026-05-18-file-manager-v2-design.md` | ⚠️ 归档 — 文件管理器v2设计(引用旧文件结构) |
|
||||
| `design/specs/2026-05-19-code-review-followup.md` | ⚠️ 归档 — PHP代码审查(前端已迁移到Tailwind) |
|
||||
| `design/plans/2026-05-16-password-presets.md` | ⚠️ 归档 — 密码预设实施计划 |
|
||||
| `design/plans/2026-05-16-simplified-push.md` | ⚠️ 归档 — 简化推送实施计划 |
|
||||
| `design/plans/2026-05-17-retry-progress.md` | ⚠️ 归档 — 重试+进度实施计划 |
|
||||
| `design/plans/2026-05-17-server-add-optimize.md` | ⚠️ 归档 — 服务器添加优化实施计划 |
|
||||
| `design/plans/2026-05-18-file-manager-v2.md` | ⚠️ 归档 — 文件管理器v2实施计划 |
|
||||
## 2. 变更与审计
|
||||
|
||||
## reports/ — 审查与工作报告
|
||||
|
||||
### Phase 2 逐行走读(2026-05-22~23)
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| **`reports/audit-phase-2-findings-matrix.md`** | **63 条 FINDING 全量状态表** |
|
||||
| `reports/audit-phase-2-walk-closure.md` | 走读闭环 |
|
||||
| `reports/audit-phase-2a`~`2j-line-walk.md` | 各批次逐行报告 |
|
||||
| `reports/2026-05-23-full-audit-report.md` | 7 部门并行初审(与 Phase 2 交叉引用) |
|
||||
| `reports/audit-full-vs-phase2-reconciliation.md` | C1–C12 与 Phase 2 对照表 |
|
||||
|
||||
### 部门审查报告
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `reports/review-cto-2026-05-21.md` | CTO审查 — 连接池/SSH/CORS/依赖(含勘误) |
|
||||
| `reports/review-deploy-architecture-2026-05-21.md` | 运维总监审查 — 部署架构/三层守护/Nginx(含勘误) |
|
||||
| `reports/review-quality-director-2026-05-21.md` | 质量总监审查 — 代码规范/datetime/测试(含勘误) |
|
||||
| `reports/step-0-infrastructure/ecc-security-report.md` | ECC组安全审查 — Redis/Schema/认证/SSH/编码(含勘误) |
|
||||
| `reports/signoff-2026-05-21.md` | 审查签字确认 |
|
||||
| `reports/session-log-2026-05-21.md` | 会话操作日志 |
|
||||
|
||||
### 实施报告
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `reports/implementation-progress-2026-05-21.md` | 6步实施进度总览 |
|
||||
| `reports/step-0-infrastructure/engineering-report.md` | Step 0: 基础设施加固 |
|
||||
| `reports/step-0-infrastructure/ag-code-review-report.md` | Step 0: 代码审查 |
|
||||
| `reports/step-0-infrastructure/testing-report.md` | Step 0: 测试报告 |
|
||||
| `reports/step-1-data-layer/engineering-report.md` | Step 1: 数据层 |
|
||||
| `reports/step-2-auth/engineering-report.md` | Step 2: 认证层 |
|
||||
| `reports/step-3-webssh/engineering-report.md` | Step 3: Web SSH(含paramiko→asyncssh迁移) |
|
||||
| `reports/step-4-sync/engineering-report.md` | Step 4: Sync引擎 |
|
||||
| `reports/step-5-frontend/engineering-report.md` | Step 5: 前端迁移 |
|
||||
| `reports/step-5-frontend/design-review-report.md` | Step 5: 前端设计审查 |
|
||||
| `reports/step-5-frontend/product-department-report.md` | Step 5: 产品部报告 |
|
||||
|
||||
## research/ — 竞品与技术调研
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `research/jumpserver-easynode-architecture-analysis.md` | JumpServer & EasyNode 架构对比分析 |
|
||||
| `research/ag-research-report.md` | AI Agent 调研报告 |
|
||||
|
||||
## changelog/ — 变更记录(强制)
|
||||
|
||||
> **纪律**:每次有意义的代码/配置修改都必须在此目录新增 `YYYY-MM-DD-<主题>.md`。见 `.cursor/rules/perfect-implementation.mdc`。
|
||||
|
||||
### Phase 2 安全与审计(2026-05-22~23)
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `changelog/2026-05-22-security-fixes-p0.md` | P0:WebSSH、中心 exec、crypto |
|
||||
| `changelog/2026-05-22-security-fixes-p1.md` | P1:注入、危险命令、Agent |
|
||||
| `changelog/2026-05-22-audit-p2-fixes.md` | P2 批次修复 |
|
||||
| `changelog/2026-05-22-global-jwt-middleware.md` | 全局 JWT 中间件 |
|
||||
| `changelog/2026-05-22-alert-push-dashboard.md` | 告警策略 + 仪表盘 |
|
||||
| `changelog/2026-05-22-telegram-sanitize-and-security-tests.md` | Telegram 脱敏 + 单测 |
|
||||
| `changelog/2026-05-22-frontend-vendor-selfhost.md` | 前端 vendor 自托管 |
|
||||
| `changelog/2026-05-22-line-walk-audit-standard.md` | 逐行走读标准 |
|
||||
|
||||
### 其他
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `changelog/2026-05-22-implementation-standards-rule.md` | 完美实现与文档纪律全局规则 |
|
||||
| `changelog/2026-05-22-tech-stack-rules-mysql-mcp.md` | 技术栈 Cursor 规则 + MySQL MCP |
|
||||
| `changelog/2026-05-22-local-mysql-full-access-node20-fix.md` | 本地库可写 + Node20 安装修复 |
|
||||
| `changelog/fix-plan-2026-05-20.md` | 代码问题修复计划(13项) |
|
||||
| `changelog/fix-plan-full-2026-05-20.md` | 全量未完成项修复计划(24项) |
|
||||
| `changelog/fix-verification-report-2026-05-20.md` | 修复验证报告 |
|
||||
|
||||
## memory/ — 项目记忆(AI辅助)
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `memory/MEMORY.md` | 记忆索引 |
|
||||
| `memory/mem_*.md` | 17个记忆文件(概述/决策/数据流/守护/模块/测试/等) |
|
||||
|
||||
## team/ — 部门协作
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `team/collaboration-charter.md` | 部门协作章程(5阶段流水线) |
|
||||
| `team/roster-2026-05-21.md` | 团队编制档案(194人/9部门) |
|
||||
| `team/roles/` | 12个部门角色定义文档 |
|
||||
|
||||
## skills/ — 团队技能定义(AI辅助)
|
||||
| 目录 | 说明 |
|
||||
|------|------|
|
||||
| `skills/` | 194个技能文件(各部门AI技能定义) |
|
||||
| [changelog/](changelog/) | 每次代码变更(**不归档、不合并**)· [INDEX](changelog/INDEX.md) |
|
||||
| [audit/](audit/) | 门控用审计 · [INDEX](audit/INDEX.md) · `2026-06-04-bug-scan-registry.json` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 活跃报告(`reports/`)
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [audit-plugin-run-final-2026-06-04.md](reports/audit-plugin-run-final-2026-06-04.md) | 插件审计总验收 |
|
||||
| [audit-frontend-14p-2026-06-04-closure.md](reports/audit-frontend-14p-2026-06-04-closure.md) | 前端 14 页 closure |
|
||||
| [audit-phase-4-plugin-closure-2026-06-04.md](reports/audit-phase-4-plugin-closure-2026-06-04.md) | Phase4 收尾 |
|
||||
| [2026-06-01-production-verification.md](reports/2026-06-01-production-verification.md) | 生产验证(待更新 L5) |
|
||||
| [2026-06-01-final-acceptance-checklist.md](reports/2026-06-01-final-acceptance-checklist.md) | 终验清单 |
|
||||
|
||||
批量 batch 报告 → [archive/audits/](archive/audits/) 合并卷。
|
||||
|
||||
---
|
||||
|
||||
## 4. 测试(全站用例 SSOT)
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [testing/test-case-matrix.md](testing/test-case-matrix.md) | **152 条** UT/IT/CH/E2E/MCP 编号用例 |
|
||||
| [testing/mcp-browser-playbook.md](testing/mcp-browser-playbook.md) | Agent Playwright MCP 点页规程 |
|
||||
| [design/specs/2026-06-08-full-site-testing-strategy-design.md](design/specs/2026-06-08-full-site-testing-strategy-design.md) | 四层金字塔设计 |
|
||||
| [design/plans/2026-06-08-full-site-testing-strategy.md](design/plans/2026-06-08-full-site-testing-strategy.md) | 分阶段实施计划 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 设计与标准
|
||||
|
||||
| 目录 | 说明 |
|
||||
|------|------|
|
||||
| [design/specs/](design/specs/) | 设计规格 |
|
||||
| [design/plans/](design/plans/) | 实施计划 |
|
||||
| [../standards/](../standards/) | 开发与审计标准 |
|
||||
| [project/standards-transfer-package.md](project/standards-transfer-package.md) | 标准转接包 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 归档区
|
||||
|
||||
**[archive/README.md](archive/README.md)** — 2026-06 审计合并卷、旧 handoff、agentmemory、2026-05 审查报告。
|
||||
|
||||
---
|
||||
|
||||
## 7. 工具脚本
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| `scripts/local_verify.sh` | L2b 本地验证 |
|
||||
| `scripts/consolidate_docs.py` | 文档归档合并(维护用) |
|
||||
| `scripts/run_nexus_acceptance.py` | L4 验收 |
|
||||
| `deploy/pre_deploy_check.sh` | 7 道门控 |
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Nexus 文档归档区
|
||||
|
||||
> 更新: 2026-06-04
|
||||
|
||||
历史审计、旧 handoff、agentmemory。**日常请先读** [docs/README.md](../README.md)。
|
||||
|
||||
## 合并卷
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| [audits/2026-06-04-line-walk-merged.md](audits/2026-06-04-line-walk-merged.md) | Line-walk batch |
|
||||
| [audits/2026-06-04-delta-merged.md](audits/2026-06-04-delta-merged.md) | Delta D1–D8 |
|
||||
| [audits/2026-06-04-phase2-merged.md](audits/2026-06-04-phase2-merged.md) | Phase2 P2 |
|
||||
| [audits/2026-06-04-frontend-14p-merged.md](audits/2026-06-04-frontend-14p-merged.md) | 前端 14p |
|
||||
| [memory/agentmemory-2026-05-merged.md](memory/agentmemory-2026-05-merged.md) | 旧 memory |
|
||||
|
||||
原文在对应子目录;changelog 不归档。
|
||||
@@ -0,0 +1,183 @@
|
||||
# Delta D1–D8 合并卷
|
||||
|
||||
> 生成日期: 2026-06-04 · 源文件 8 篇 · 原文 `docs/archive/audits/2026-06-04-delta/`
|
||||
|
||||
## 批次索引
|
||||
|
||||
### `audit-delta-2026-06-04-waveD1.md`
|
||||
|
||||
# Phase 1 Delta 审计 — Wave D1(同步/后台)
|
||||
## 登记
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
## 入口表(sync_v2.py API)
|
||||
## Closure 表
|
||||
**len(H)=19, Closure=19**
|
||||
## FINDING(本波)
|
||||
无 P0/P1 新 FINDING。RISK 项与既有运维/单人决策一致,不阻塞。
|
||||
## DoD
|
||||
- [x] len(H) == Closure
|
||||
- [x] FINDING 含行号
|
||||
## 验证
|
||||
|
||||
### `audit-delta-2026-06-04-waveD2.md`
|
||||
|
||||
# Phase 1 Delta 审计 — Wave D2(认证 / 安装 / WS / Agent)
|
||||
## 登记
|
||||
## 入口表
|
||||
### install.py
|
||||
### auth.py
|
||||
### websocket.py
|
||||
### agent.py
|
||||
## Closure 表
|
||||
**len(H)=22, Closure=22**
|
||||
## FINDING
|
||||
无 P0/P1 新 FINDING。RISK:WS query JWT、Agent global key — 见 `docs/project/risk-acceptance-single-operator.md`。
|
||||
## DoD
|
||||
|
||||
### `audit-delta-2026-06-04-waveD3.md`
|
||||
|
||||
# Phase 1 Delta 审计 — Wave D3(服务器 / 设置 / 脚本 / WebSSH)
|
||||
## 登记
|
||||
## 入口表(摘要)
|
||||
### webssh.py
|
||||
### servers.py(主要写操作)
|
||||
### settings.py
|
||||
### scripts.py
|
||||
## Closure 表
|
||||
**len(H)=24, Closure=24**
|
||||
## FINDING
|
||||
无 P0/P1 新 FINDING。
|
||||
## DoD
|
||||
|
||||
### `audit-delta-2026-06-04-waveD4.md`
|
||||
|
||||
# Phase 1 Delta 审计 — Wave D4(前端 Push)
|
||||
## 登记
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
## 入口表(前端 → API)
|
||||
## Closure 表
|
||||
**len(H)=17, Closure=17**
|
||||
## FINDING
|
||||
无 P0/P1 新 FINDING。
|
||||
## DoD
|
||||
- [x] len(H) == Closure
|
||||
## 验证
|
||||
|
||||
### `audit-delta-2026-06-04-waveD5.md`
|
||||
|
||||
# Phase 1 Delta 审计 — Wave D5(前端 Files / Terminal)
|
||||
## 登记
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
## 入口表(前端 → API / WS)
|
||||
## Closure 表
|
||||
**len(H)=18, Closure=18**
|
||||
## FINDING
|
||||
无 P0/P1 新 FINDING。
|
||||
## DoD
|
||||
- [x] len(H) == Closure
|
||||
## 验证
|
||||
|
||||
### `audit-delta-2026-06-04-waveD6.md`
|
||||
|
||||
# Phase 1 Delta 审计 — Wave D6(认证链 / Login)
|
||||
## 登记
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
## 入口表
|
||||
## Closure 表
|
||||
**len(H)=19, Closure=19**
|
||||
## FINDING
|
||||
无 P0/P1 新 FINDING。
|
||||
## DoD
|
||||
- [x] len(H) == Closure
|
||||
## 验证
|
||||
|
||||
### `audit-delta-2026-06-04-waveD7.md`
|
||||
|
||||
# Phase 1 Delta 审计 — Wave D7(Deploy / 本地验证脚本)
|
||||
**标准**: 变更审查(运维/门控;非业务 API 8 步,沿用 Closure 表格式)
|
||||
## 登记
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
## 入口表(脚本调用链)
|
||||
## Closure 表
|
||||
**len(H)=16, Closure=16**
|
||||
## FINDING
|
||||
无 P0/P1 新 FINDING。
|
||||
## DoD
|
||||
- [x] len(H) == Closure
|
||||
## 验证
|
||||
|
||||
### `audit-delta-2026-06-04-waveD8-closure.md`
|
||||
|
||||
# Phase 1 Delta 审计 — Wave D8(汇总 Closure)
|
||||
**标准**: Phase 1 收尾 DoD
|
||||
## 汇总表
|
||||
| 波次 | 主题 | 文件数 | H | P0/P1 | 报告 |
|
||||
## 已接受 RISK(跨波重复,不重复开 FINDING)
|
||||
## Registry 更新
|
||||
## Step 3 / Closure / DoD
|
||||
- **len(H) 汇总** = 135,各波 Closure 均已 100% 对齐
|
||||
- **无 P0/P1** 待修项进入 Phase 2
|
||||
## 下一步(计划 Phase 2 / L3)
|
||||
## 验证
|
||||
|
||||
## Closure 摘录(合并)
|
||||
|
||||
| 1 | `server/api/sync_v2.py` | Python | 1656 | 1–200, 680–1080, 1280–1657 + 全文件 `@router`/grep H |
|
||||
| 2 | `server/application/services/sync_engine_v2.py` | Python | 552 | 1–552 全文 |
|
||||
| 3 | `server/background/schedule_runner.py` | Python | 262 | 1–262 全文 |
|
||||
| 4 | `server/background/retry_runner.py` | Python | 169 | 1–169 全文 |
|
||||
| 5 | `server/background/heartbeat_flush.py` | Python | 101 | 1–101 全文 |
|
||||
| H | 文件:行号 | 判定 | 理由 |
|
||||
| H-Install-lock | install.py:344-350 | SAFE | `_reject_if_locked` 阻断突变 |
|
||||
| H-Install-token | install.py:314-327 | SAFE | `secrets.compare_digest` 一次性 token |
|
||||
| H-Install-token | install.py:601 | SAFE | create-admin 必须 token |
|
||||
| H-Install-SQL | install.py:540-544 | SAFE | 参数化 INSERT settings |
|
||||
| H-Install-DDL | install.py:489-503 | SAFE | DDL 幂等 except pass |
|
||||
| H-Install-subprocess | install.py:273-293 | SAFE | crontab/supervisor 固定 argv |
|
||||
| H-Auth-brute | auth.py:125-138 | SAFE | 429/202 由 AuthService |
|
||||
| H-Auth-cookie | auth.py:38-54 | SAFE | HttpOnly + Secure(HTTPS) + SameSite=Lax |
|
||||
| H-Auth-TOTP-IDOR | auth.py:223-259 | SAFE | admin_id 必须等于 JWT sub |
|
||||
| H-Auth-password | auth.py:288-305 | SAFE | bcrypt + token_version 失效 |
|
||||
| H-Auth-webssh | auth.py:330-349 | SAFE | 短效 server-bound token |
|
||||
| H-JWT-middleware | auth_jwt.py:78-146 | SAFE | ASGI 层拦截 /api/* |
|
||||
| H-JWT-public | auth_jwt.py:38-50 | SAFE | 公开前缀含 agent/install/ws |
|
||||
| H-JWT-tv | auth_jwt.py:222-226 | SAFE | token_version 精确匹配 |
|
||||
| H-WS-auth | websocket.py:303-311 | SAFE | 无 token → 4001;无效 → 4401 |
|
||||
| H-WS-auth | websocket.py:366-407 | SAFE | tv + updated_at 双检 |
|
||||
| H-WS-ADR011 | websocket.py:9,300 | RISK | JWT 在 query;单人运维已接受 ADR-011 |
|
||||
| H-Agent-key | agent.py:40-57 | SAFE | compare_digest;handler 二次验 per-server |
|
||||
| H-Agent-unknown | agent.py:193-199 | SAFE | 未知 server 200 discard,不写入 |
|
||||
| H-Agent-global | agent.py:84-92 | RISK | legacy global API_KEY;risk-acceptance 文档 |
|
||||
| H-Agent-callback | agent.py:324-347 | SAFE | rate limit + job secret |
|
||||
| H-Agent-no-exec | agent.py:378 | SAFE | 控制面无 shell exec |
|
||||
| POST | `/{id}/install-agent` 等 | JWT + SSH |
|
||||
| 文件 | H 命中 |
|
||||
| usePushForm.ts | H-Auth, H-Upload, H-Path |
|
||||
| usePushPreview.ts | H-Auth, H-Path |
|
||||
| usePushProgress.ts | H-Auth, H-WS, H-Batch |
|
||||
| PushZipUpload.vue | H-Upload, H-XSS |
|
||||
| PushServerGrid.vue | H-XSS |
|
||||
| useFilesActions.ts | H-Auth, H-Path, H-Delete, H-Upload |
|
||||
| useFilesEditor.ts | H-Auth, H-Size |
|
||||
| useFilesPermissions.ts | H-Auth, H-Sudo |
|
||||
| useTerminalSessions.ts | H-Auth, H-WS, H-Path, H-Cmd |
|
||||
| useTerminalCmdBar.ts | H-Storage, H-Cmd |
|
||||
| LoginPage.vue | H-Secret, H-Redirect, H-Lockout, H-XSS |
|
||||
| auth.ts | H-Token, H-Refresh, H-Profile |
|
||||
| api/index.ts | H-Auth, H-Refresh, H-401 |
|
||||
| router/index.ts | H-Guard, H-Redirect |
|
||||
| wsUrl.ts | H-WS |
|
||||
| pre_deploy_check.sh | H-Gate, H-Secret, H-Git |
|
||||
| local_verify.sh | H-Chain |
|
||||
| local_integration_smoke.sh | H-Health, H-Auth |
|
||||
| start-dev.sh | H-DevBind, H-Env |
|
||||
| docker-compose.yml | H-Port, H-DefaultCreds |
|
||||
| 波次 | 主题 | 文件数 | H | P0/P1 | 报告 |
|
||||
| D1 | sync/后台 | 5 | 19 | 0 | [waveD1](audit-delta-2026-06-04-waveD1.md) |
|
||||
| D2 | auth/agent/ws | 5 | 22 | 0 | [waveD2](audit-delta-2026-06-04-waveD2.md) |
|
||||
| D3 | servers/settings/scripts/webssh | 5 | 24 | 0 | [waveD3](audit-delta-2026-06-04-waveD3.md) |
|
||||
| D4 | 前端 Push | 5 | 17 | 0 | [waveD4](audit-delta-2026-06-04-waveD4.md) |
|
||||
| D5 | 前端 Files/Terminal | 5 | 18 | 0 | [waveD5](audit-delta-2026-06-04-waveD5.md) |
|
||||
| D6 | 认证链 Login | 5 | 19 | 0 | [waveD6](audit-delta-2026-06-04-waveD6.md) |
|
||||
| D7 | Deploy/本地脚本 | 5 | 16 | 0 | [waveD7](audit-delta-2026-06-04-waveD7.md) |
|
||||
| **合计** | | **35** | **135** | **0** | |
|
||||
@@ -0,0 +1,95 @@
|
||||
# Phase 1 Delta 审计 — Wave D1(同步/后台)
|
||||
|
||||
**日期**: 2026-06-04
|
||||
**范围**: 推送/sync 核心路径(Code Review batch1 后 delta)
|
||||
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
|
||||
|
||||
## 登记
|
||||
|
||||
| # | 文件 | 语言 | 行数 N | Read 范围 |
|
||||
|---|------|------|--------|-----------|
|
||||
| 1 | `server/api/sync_v2.py` | Python | 1656 | 1–200, 680–1080, 1280–1657 + 全文件 `@router`/grep H |
|
||||
| 2 | `server/application/services/sync_engine_v2.py` | Python | 552 | 1–552 全文 |
|
||||
| 3 | `server/background/schedule_runner.py` | Python | 262 | 1–262 全文 |
|
||||
| 4 | `server/background/retry_runner.py` | Python | 169 | 1–169 全文 |
|
||||
| 5 | `server/background/heartbeat_flush.py` | Python | 101 | 1–101 全文 |
|
||||
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
|
||||
| 文件 | H 命中 |
|
||||
|------|--------|
|
||||
| sync_v2.py | H-Shell, H-Auth, H-Path, H-Audit, H-Upload |
|
||||
| sync_engine_v2.py | H-Shell, H-AuthN/A, H-Concurrency |
|
||||
| schedule_runner.py | H-Cron, H-Script-exec, H-Lock |
|
||||
| retry_runner.py | H-Lock, H-Retry |
|
||||
| heartbeat_flush.py | H-Redis, H-DB |
|
||||
|
||||
## 入口表(sync_v2.py API)
|
||||
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| POST | `/api/sync/files` | JWT `get_current_admin` |
|
||||
| POST | `/api/sync/preview` | 同上 |
|
||||
| POST | `/api/sync/file-ops` | 同上 |
|
||||
| POST | `/api/sync/file-clipboard` | 同上 |
|
||||
| POST | `/api/sync/browse` | 同上 |
|
||||
| POST | `/api/sync/browse-local` | 同上 |
|
||||
| POST | `/api/sync/local-file-ops` | 同上 |
|
||||
| POST | `/api/sync/local-file-preview` | 同上 |
|
||||
| POST | `/api/sync/cancel` | 同上 |
|
||||
| POST | `/api/sync/reconcile-stale-logs` | 同上 |
|
||||
| POST | `/api/sync/validate-source-path` | 同上 |
|
||||
| POST | `/api/sync/diagnose` | 同上 |
|
||||
| POST | `/api/sync/file-diff` | 同上 |
|
||||
| POST | `/api/sync/upload` | 同上 |
|
||||
| POST | `/api/sync/upload-zip` | 同上 |
|
||||
| POST | `/api/sync/verify` | 同上 |
|
||||
| POST | `/api/sync/download` | 同上 |
|
||||
| POST | `/api/sync/read-file` | 同上 |
|
||||
| POST | `/api/sync/write-file` | 同上 |
|
||||
| POST | `/api/sync/chmod` | 同上 |
|
||||
| POST | `/api/sync/compress` | 同上 |
|
||||
| POST | `/api/sync/decompress` | 同上 |
|
||||
|
||||
## Closure 表
|
||||
|
||||
| H | 文件:行号 | 判定 | 理由 |
|
||||
|---|-----------|------|------|
|
||||
| H-Shell | sync_v2.py:156-173 | SAFE | 远程路径 `normalize_remote_abs_path` + `shlex.quote` |
|
||||
| H-Shell | sync_v2.py:777 | SAFE | `remote_file` 经 `remote_join`+quote;`wc -c && cat` 单路径 |
|
||||
| H-Path | sync_v2.py:721-731 | SAFE | `ensure_under_nexus_upload` + prefix 校验 |
|
||||
| H-Path | sync_v2.py:887-890 | SAFE | 上传文件名去斜杠 |
|
||||
| H-Path | sync_v2.py:999-1005 | SAFE | ZIP slip:`assert_zip_member_safe` |
|
||||
| H-Auth | sync_v2.py:74-1610 | SAFE | 全部 22 路由 `Depends(get_current_admin)` |
|
||||
| H-Audit | sync_v2.py:119-122 等 | SAFE | CUD/敏感操作 `_audit_sync` |
|
||||
| H-Upload | sync_v2.py:876-877 | SAFE | 100MB 上限 |
|
||||
| H-Upload | sync_v2.py:977-978 | SAFE | ZIP 500MB 上限 |
|
||||
| H-Shell | sync_engine_v2.py:433-465 | RISK | `StrictHostKeyChecking=no` 运维惯例;单人环境已接受 |
|
||||
| H-Shell | sync_engine_v2.py:440-445 | RISK | sshpass/SSHPASS;内部工具,凭据加密存储 |
|
||||
| H-Concurrency | sync_engine_v2.py:89-150 | SAFE | `_db_lock` 串行化 sync_log 写 |
|
||||
| H-Cron | schedule_runner.py:58-72 | RISK | 畸形 cron 字段 `int()` 可抛 ValueError;外层 254 行捕获 rollback |
|
||||
| H-Script | schedule_runner.py:198-205 | SAFE | 调度脚本仅 JWT 管理员配置的 schedule 触发 |
|
||||
| H-Lock | schedule_runner.py:125-131 | SAFE | `FOR UPDATE SKIP LOCKED` 防双触发 |
|
||||
| H-Lock | retry_runner.py:67-76 | SAFE | SKIP LOCKED 认领 |
|
||||
| H-Retry | retry_runner.py:22-59 | SAFE | 指数退避 + max_retries |
|
||||
| H-Redis | heartbeat_flush.py:42-48 | SAFE | scan 失败 log+continue |
|
||||
| H-DB | heartbeat_flush.py:57-99 | SAFE | 参数化 ORM update;未知 server_id rowcount 0 跳过 |
|
||||
|
||||
**len(H)=19, Closure=19**
|
||||
|
||||
## FINDING(本波)
|
||||
|
||||
无 P0/P1 新 FINDING。RISK 项与既有运维/单人决策一致,不阻塞。
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] len(H) == Closure
|
||||
- [x] Read 覆盖 1..N(sync_v2 大文件分段 + 全路由 grep)
|
||||
- [x] FINDING 含行号
|
||||
- [x] 入口表 + sink 标注
|
||||
- [x] 逐行完成 ✓(D1 五文件)
|
||||
|
||||
## 验证
|
||||
|
||||
- `docker-compose.yml` Redis 发布 6379(修复本地 uvicorn 连 Redis)
|
||||
- 待跑:`bash scripts/local_verify.sh`
|
||||
@@ -0,0 +1,100 @@
|
||||
# Phase 1 Delta 审计 — Wave D2(认证 / 安装 / WS / Agent)
|
||||
|
||||
**日期**: 2026-06-04
|
||||
**范围**: Code Review batch2 后 delta — 认证与 Agent 路径
|
||||
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
|
||||
|
||||
## 登记
|
||||
|
||||
| # | 文件 | 语言 | 行数 N | Read 范围 |
|
||||
|---|------|------|--------|-----------|
|
||||
| 1 | `server/api/install.py` | Python | 746 | 1–746 全文 |
|
||||
| 2 | `server/api/auth.py` | Python | 374 | 1–374 全文 |
|
||||
| 3 | `server/api/auth_jwt.py` | Python | 274 | 1–274 全文 |
|
||||
| 4 | `server/api/websocket.py` | Python | 552 | 1–552 全文 |
|
||||
| 5 | `server/api/agent.py` | Python | 378 | 1–378 全文 |
|
||||
|
||||
## 入口表
|
||||
|
||||
### install.py
|
||||
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| GET | `/api/install/status` | 无(已锁定 404) |
|
||||
| GET | `/api/install/env-check` | 安装模式 + 未 lock |
|
||||
| POST | `/api/install/init-db` | 无 .env + 未 lock |
|
||||
| POST | `/api/install/create-admin` | `install_token` |
|
||||
| POST | `/api/install/lock` | 需已有 admin |
|
||||
| GET | `/api/install/state` | 未 lock |
|
||||
|
||||
### auth.py
|
||||
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| POST | `/api/auth/login` | 公开 |
|
||||
| POST | `/api/auth/refresh` | Cookie/body refresh |
|
||||
| POST | `/api/auth/logout` | Cookie |
|
||||
| POST | `/api/auth/totp/*` | JWT + self admin_id |
|
||||
| PUT | `/api/auth/password` | JWT + 当前密码 (+TOTP) |
|
||||
| POST | `/api/auth/webssh-token` | JWT + server 存在 |
|
||||
| GET | `/api/auth/me` | JWT |
|
||||
|
||||
### websocket.py
|
||||
|
||||
| 类型 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| WS | `/ws/alerts` | Query `?token=` JWT |
|
||||
| WS | `/ws/sync` | 同上 |
|
||||
|
||||
### agent.py
|
||||
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| POST | `/api/agent/heartbeat` | X-API-Key + per-server key |
|
||||
| POST | `/api/agent/script-callback` | X-API-Key + job secret + rate limit |
|
||||
|
||||
## Closure 表
|
||||
|
||||
| H | 文件:行号 | 判定 | 理由 |
|
||||
|---|-----------|------|------|
|
||||
| H-Install-lock | install.py:344-350 | SAFE | `_reject_if_locked` 阻断突变 |
|
||||
| H-Install-token | install.py:314-327 | SAFE | `secrets.compare_digest` 一次性 token |
|
||||
| H-Install-token | install.py:601 | SAFE | create-admin 必须 token |
|
||||
| H-Install-SQL | install.py:540-544 | SAFE | 参数化 INSERT settings |
|
||||
| H-Install-DDL | install.py:489-503 | SAFE | DDL 幂等 except pass |
|
||||
| H-Install-subprocess | install.py:273-293 | SAFE | crontab/supervisor 固定 argv |
|
||||
| H-Auth-brute | auth.py:125-138 | SAFE | 429/202 由 AuthService |
|
||||
| H-Auth-cookie | auth.py:38-54 | SAFE | HttpOnly + Secure(HTTPS) + SameSite=Lax |
|
||||
| H-Auth-TOTP-IDOR | auth.py:223-259 | SAFE | admin_id 必须等于 JWT sub |
|
||||
| H-Auth-password | auth.py:288-305 | SAFE | bcrypt + token_version 失效 |
|
||||
| H-Auth-webssh | auth.py:330-349 | SAFE | 短效 server-bound token |
|
||||
| H-JWT-middleware | auth_jwt.py:78-146 | SAFE | ASGI 层拦截 /api/* |
|
||||
| H-JWT-public | auth_jwt.py:38-50 | SAFE | 公开前缀含 agent/install/ws |
|
||||
| H-JWT-tv | auth_jwt.py:222-226 | SAFE | token_version 精确匹配 |
|
||||
| H-WS-auth | websocket.py:303-311 | SAFE | 无 token → 4001;无效 → 4401 |
|
||||
| H-WS-auth | websocket.py:366-407 | SAFE | tv + updated_at 双检 |
|
||||
| H-WS-ADR011 | websocket.py:9,300 | RISK | JWT 在 query;单人运维已接受 ADR-011 |
|
||||
| H-Agent-key | agent.py:40-57 | SAFE | compare_digest;handler 二次验 per-server |
|
||||
| H-Agent-unknown | agent.py:193-199 | SAFE | 未知 server 200 discard,不写入 |
|
||||
| H-Agent-global | agent.py:84-92 | RISK | legacy global API_KEY;risk-acceptance 文档 |
|
||||
| H-Agent-callback | agent.py:324-347 | SAFE | rate limit + job secret |
|
||||
| H-Agent-no-exec | agent.py:378 | SAFE | 控制面无 shell exec |
|
||||
|
||||
**len(H)=22, Closure=22**
|
||||
|
||||
## FINDING
|
||||
|
||||
无 P0/P1 新 FINDING。RISK:WS query JWT、Agent global key — 见 `docs/project/risk-acceptance-single-operator.md`。
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] len(H) == Closure
|
||||
- [x] Read 1..N 全文
|
||||
- [x] 入口表 + sink
|
||||
- [x] 逐行完成 ✓(D2 五文件)
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
bash scripts/local_verify.sh # 预期全绿
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
# Phase 1 Delta 审计 — Wave D3(服务器 / 设置 / 脚本 / WebSSH)
|
||||
|
||||
**日期**: 2026-06-04
|
||||
**范围**: Code Review batch2–3 相关路径
|
||||
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
|
||||
|
||||
## 登记
|
||||
|
||||
| # | 文件 | 语言 | 行数 N | Read 范围 |
|
||||
|---|------|------|--------|-----------|
|
||||
| 1 | `server/api/webssh.py` | Python | 398 | 1–398 全文 |
|
||||
| 2 | `server/api/servers.py` | Python | 1741 | 1–250, 1059–1290, 1681–1742 + 全路由 grep |
|
||||
| 3 | `server/api/settings.py` | Python | 1403 | 1–130, 280–384, 562–730, 881–930 + grep |
|
||||
| 4 | `server/application/services/script_service.py` | Python | 734 | 1–734 全文 |
|
||||
| 5 | `server/api/scripts.py` | Python | 418 | 1–418 全文 |
|
||||
|
||||
## 入口表(摘要)
|
||||
|
||||
### webssh.py
|
||||
| 类型 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| WS | `/ws/terminal/{server_id}` | JWT `purpose=webssh` + server_id 绑定 |
|
||||
|
||||
### servers.py(主要写操作)
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| POST/PUT/DELETE | `/api/servers/`… | JWT |
|
||||
| POST | `/{id}/agent-key` | JWT + 当前密码 |
|
||||
| POST | `/{id}/install-agent` 等 | JWT + SSH |
|
||||
|
||||
### settings.py
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| GET/PUT | `/api/settings/*` | JWT;IMMUTABLE_KEYS 403 |
|
||||
| POST | `/api-key/reveal`, `/telegram/reveal-token` | JWT + re-auth |
|
||||
|
||||
### scripts.py
|
||||
| 方法 | 路径 | 鉴权 |
|
||||
|------|------|------|
|
||||
| CRUD/exec | `/api/scripts/*` | JWT 全路由 |
|
||||
|
||||
## Closure 表
|
||||
|
||||
| H | 文件:行号 | 判定 | 理由 |
|
||||
|---|-----------|------|------|
|
||||
| H-WS-auth | webssh.py:96-109 | SAFE | webssh JWT + server_id 必须匹配路径 |
|
||||
| H-WS-IDOR | webssh.py:106-108 | SAFE | 非绑定 token → 4003 |
|
||||
| H-WS-cred | webssh.py:127-135 | SAFE | SSH 凭据未配置则拒绝 |
|
||||
| H-WS-audit | webssh.py:137-157 | SAFE | connect/disconnect 审计 |
|
||||
| H-WS-cmdlog | webssh.py:276-290 | SAFE | 命令日志 + dangerous check |
|
||||
| H-WS-ADR011 | webssh.py:92 | RISK | query token;已接受 |
|
||||
| H-Server-mask | servers.py:1692-1698 | SAFE | password_set / 密钥不返回明文 |
|
||||
| H-Server-encrypt | servers.py:1096-1099 | SAFE | Fernet 加密存储 |
|
||||
| H-Server-allowlist | servers.py:1163-1187 | SAFE | UPDATE 字段白名单 |
|
||||
| H-Server-agent-key | servers.py:1251-1288 | SAFE | re-auth 后生成/返回一次 |
|
||||
| H-Server-shell | servers.py:631-633 | SAFE | install cmd shlex.quote |
|
||||
| H-Server-sudo | servers.py:36-60 | RISK | 临时 sudoers;运维必要 |
|
||||
| H-Settings-immutable | settings.py:331-334 | SAFE | api_key 等不可 PUT |
|
||||
| H-Settings-mask | settings.py:114-128 | SAFE | GET 敏感项 value 空 + value_set |
|
||||
| H-Settings-reauth | settings.py:93-98 | SAFE | reveal 需 bcrypt |
|
||||
| H-Settings-SSRF | settings.py:68-88 | SAFE | 订阅 URL 私有 IP 拦截 |
|
||||
| H-Settings-PUT-leak | settings.py:383 | RISK | PUT 响应可能含 telegram 明文;JWT 已认证 |
|
||||
| H-Script-danger | scripts.py:352-359 | SAFE | check_dangerous_command 拒绝 |
|
||||
| H-Script-cred | scripts.py:407-418 | SAFE | 凭据 API 无 password 字段 |
|
||||
| H-Script-allowlist | scripts.py:111-114,303-306 | SAFE | credential/script UPDATE 白名单 |
|
||||
| H-Svc-redact | script_service.py:108-111 | SAFE | 执行记录 command 脱敏 DB_PASS |
|
||||
| H-Svc-ssh | script_service.py:680-698 | SAFE | SSH exec 经 pool |
|
||||
| H-Svc-kill | script_service.py:306-322 | SAFE | kill 命令 pid/log 路径校验 |
|
||||
| H-Svc-server-map | script_service.py:519-523 | SAFE | 预加载 server_map 避免并行竞态 |
|
||||
|
||||
**len(H)=24, Closure=24**
|
||||
|
||||
## FINDING
|
||||
|
||||
无 P0/P1 新 FINDING。
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] len(H) == Closure
|
||||
- [x] Read 覆盖(大文件分段 + 全路由)
|
||||
- [x] 逐行完成 ✓(D3 五文件)
|
||||
|
||||
## 验证
|
||||
|
||||
`bash scripts/local_verify.sh` — 预期全绿
|
||||
@@ -0,0 +1,74 @@
|
||||
# Phase 1 Delta 审计 — Wave D4(前端 Push)
|
||||
|
||||
**日期**: 2026-06-04
|
||||
**范围**: Code Review 后 push 页 composables + 组件
|
||||
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
|
||||
|
||||
## 登记
|
||||
|
||||
| # | 文件 | 语言 | 行数 N | Read 范围 |
|
||||
|---|------|------|--------|-----------|
|
||||
| 1 | `frontend/src/composables/push/usePushForm.ts` | TS | 101 | 1–101 全文 |
|
||||
| 2 | `frontend/src/composables/push/usePushPreview.ts` | TS | 104 | 1–104 全文 |
|
||||
| 3 | `frontend/src/composables/push/usePushProgress.ts` | TS | 294 | 1–294 全文 |
|
||||
| 4 | `frontend/src/components/push/PushZipUpload.vue` | Vue | 51 | 1–51 全文 |
|
||||
| 5 | `frontend/src/components/push/PushServerGrid.vue` | Vue | 110 | 1–110 全文 |
|
||||
|
||||
**关联(本波未单独登记)**: `usePushPage.ts` 仅编排 lifecycle,无独立 sink。
|
||||
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
|
||||
| 文件 | H 命中 |
|
||||
|------|--------|
|
||||
| usePushForm.ts | H-Auth, H-Upload, H-Path |
|
||||
| usePushPreview.ts | H-Auth, H-Path |
|
||||
| usePushProgress.ts | H-Auth, H-WS, H-Batch |
|
||||
| PushZipUpload.vue | H-Upload, H-XSS |
|
||||
| PushServerGrid.vue | H-XSS |
|
||||
|
||||
## 入口表(前端 → API)
|
||||
|
||||
| 调用方 | 方法 | 路径 | 鉴权 |
|
||||
|--------|------|------|------|
|
||||
| usePushForm | POST multipart | `/sync/upload-zip` | http JWT |
|
||||
| usePushPreview | POST | `/sync/preview` | http JWT |
|
||||
| usePushProgress | POST | `/sync/files`, `/sync/cancel` | http JWT |
|
||||
| usePushProgress | WS | `/ws/sync?token=` | JWT query(已接受 RISK) |
|
||||
|
||||
## Closure 表
|
||||
|
||||
| H | 文件:行号 | 判定 | 理由 |
|
||||
|---|-----------|------|------|
|
||||
| H-Auth | usePushForm.ts:65-67 | SAFE | 经 `@/api` http,Bearer JWT |
|
||||
| H-Upload | usePushForm.ts:52,57 | RISK | 客户端仅 `.zip` 后缀;服务端 ZIP slip/大小限制(D1) |
|
||||
| H-Path | usePushForm.ts:69-72 | SAFE | `source_path` 来自服务端 upload-zip 响应 |
|
||||
| H-Path | usePushForm.ts:38-40 | SAFE | target_path 必填;远程校验在 sync_v2 |
|
||||
| H-Auth | usePushPreview.ts:28-38 | SAFE | preview POST JWT + server_id 参数化 |
|
||||
| H-Concurrency | usePushPreview.ts:72-77 | SAFE | `PREVIEW_CONCURRENCY=4` 限流 |
|
||||
| H-Auth | usePushProgress.ts:237-243 | SAFE | push POST JWT + batch_id |
|
||||
| H-WS | usePushProgress.ts:137-142 | RISK | query token;单人运维已接受 |
|
||||
| H-WS | usePushProgress.ts:79-80 | SAFE | batch_id 过滤防串批 |
|
||||
| H-WS | usePushProgress.ts:165-167 | SAFE | 4401 → forceLogout |
|
||||
| H-Batch | usePushProgress.ts:244-246 | SAFE | 服务端 batch_id 可覆盖客户端 |
|
||||
| H-Cancel | usePushProgress.ts:194-209 | SAFE | cancel 带 batch_id |
|
||||
| H-Timer | usePushProgress.ts:261-275 | SAFE | 300s 安全超时 + dispose 清理 |
|
||||
| H-Secret | usePushForm/Preview/Progress | SAFE | 无 localStorage 密码/密钥 |
|
||||
| H-XSS | PushZipUpload.vue:22 | SAFE | `{{ uploadedZip.name }}` 文本插值 |
|
||||
| H-XSS | PushServerGrid.vue:66-77 | SAFE | 无 v-html;状态 chip 文本插值 |
|
||||
| H-Upload | PushZipUpload.vue:18 | SAFE | `accept=".zip"` + 父 composable 后缀检查 |
|
||||
|
||||
**len(H)=17, Closure=17**
|
||||
|
||||
## FINDING
|
||||
|
||||
无 P0/P1 新 FINDING。
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] len(H) == Closure
|
||||
- [x] 五文件全文 Read
|
||||
- [x] 无 v-html / localStorage 凭据
|
||||
|
||||
## 验证
|
||||
|
||||
`bash scripts/local_verify.sh` — 全绿
|
||||
@@ -0,0 +1,80 @@
|
||||
# Phase 1 Delta 审计 — Wave D5(前端 Files / Terminal)
|
||||
|
||||
**日期**: 2026-06-04
|
||||
**范围**: files/terminal composables(Code Review 后 delta)
|
||||
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
|
||||
|
||||
## 登记
|
||||
|
||||
| # | 文件 | 语言 | 行数 N | Read 范围 |
|
||||
|---|------|------|--------|-----------|
|
||||
| 1 | `frontend/src/composables/files/useFilesActions.ts` | TS | 609 | 1–609 全文 |
|
||||
| 2 | `frontend/src/composables/files/useFilesEditor.ts` | TS | 163 | 1–163 全文 |
|
||||
| 3 | `frontend/src/composables/files/useFilesPermissions.ts` | TS | 101 | 1–101 全文 |
|
||||
| 4 | `frontend/src/composables/terminal/useTerminalSessions.ts` | TS | 749 | 1–749 全文 |
|
||||
| 5 | `frontend/src/composables/terminal/useTerminalCmdBar.ts` | TS | 108 | 1–108 全文 |
|
||||
|
||||
**关联**: `types.ts` 中 `sanitizeRemotePath`(40–43)已纳入 terminal 路径 H 判定。
|
||||
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
|
||||
| 文件 | H 命中 |
|
||||
|------|--------|
|
||||
| useFilesActions.ts | H-Auth, H-Path, H-Delete, H-Upload |
|
||||
| useFilesEditor.ts | H-Auth, H-Size |
|
||||
| useFilesPermissions.ts | H-Auth, H-Sudo |
|
||||
| useTerminalSessions.ts | H-Auth, H-WS, H-Path, H-Cmd |
|
||||
| useTerminalCmdBar.ts | H-Storage, H-Cmd |
|
||||
|
||||
## 入口表(前端 → API / WS)
|
||||
|
||||
| 调用方 | 方法 | 路径 | 鉴权 |
|
||||
|--------|------|------|------|
|
||||
| useFilesActions | POST | `/sync/file-ops`, `/sync/write-file`, `/sync/chmod`, `/sync/compress`, `/sync/decompress`, `/sync/upload`, `/sync/download` | http JWT |
|
||||
| useFilesEditor | 经 `readRemoteFileContent` | `/sync/read-file` | http JWT |
|
||||
| useFilesPermissions | GET/POST | `/servers/{id}/files-capability`, `setup-files-sudo` | http JWT |
|
||||
| useTerminalSessions | POST | `/auth/webssh-token` | http JWT |
|
||||
| useTerminalSessions | WS | `/ws/terminal/{id}?token=` | purpose=webssh JWT |
|
||||
| useTerminalCmdBar | WS DATA | 经 `sendWsData` | 同上 |
|
||||
|
||||
## Closure 表
|
||||
|
||||
| H | 文件:行号 | 判定 | 理由 |
|
||||
|---|-----------|------|------|
|
||||
| H-Auth | useFilesActions.ts:255-494 | SAFE | 全部经 `@/api` JWT |
|
||||
| H-Path | useFilesActions.ts:297-301,461-464 | SAFE | `validatePathSegment` 拦截 `/` `..` |
|
||||
| H-Path | useFilesActions.ts:340-347 | RISK | rename 未调 validatePathSegment;后端 sync_v2 校验 |
|
||||
| H-Path | useFilesActions.ts:71-78,119 | SAFE | `normalizeRemotePath` / `joinRemotePath` |
|
||||
| H-Delete | useFilesActions.ts:393-414 | SAFE | confirm + 逐条 delete;失败计数 |
|
||||
| H-Upload | useFilesActions.ts:486-494 | SAFE | server_id + remote_path 参数化 POST |
|
||||
| H-Auth | useFilesEditor.ts:94-98 | SAFE | readRemoteFileContent + server_id |
|
||||
| H-Size | useFilesEditor.ts:63-70 | SAFE | 2MB 编辑上限 `FILE_EDITOR_MAX_BYTES` |
|
||||
| H-Auth | useFilesPermissions.ts:65-78 | SAFE | JWT GET/POST |
|
||||
| H-Sudo | useFilesPermissions.ts:73-91 | SAFE | 一键 sudo 走后端 re-auth+SSH;无密码回传 |
|
||||
| H-Storage | useFilesPermissions.ts:56 | SAFE | sessionStorage 仅 hint dismissed 标记 |
|
||||
| H-Auth | useTerminalSessions.ts:279-281 | SAFE | webssh-token 绑定 server_id |
|
||||
| H-WS | useTerminalSessions.ts:293-294 | RISK | query token;单人运维已接受 |
|
||||
| H-WS | useTerminalSessions.ts:350-363 | SAFE | 4001/4003 分支处理 |
|
||||
| H-Path | useTerminalSessions.ts:159-163,513,578 | RISK | `sanitizeRemotePath` 未拦 `;|&$`;cd 字符串拼接;管理员会话 |
|
||||
| H-Cmd | useTerminalSessions.ts:648-655 | SAFE | 快捷命令来自 API/内置白名单 |
|
||||
| H-Cmd | useTerminalCmdBar.ts:21-34 | SAFE | 命令经 WS DATA;后端 dangerous check |
|
||||
| H-Storage | useTerminalCmdBar.ts:7-33 | RISK | localStorage 存命令历史(非密码);UX 便利 |
|
||||
| H-Secret | 五文件 | SAFE | 无 password/api_key 存 localStorage |
|
||||
|
||||
**len(H)=18, Closure=18**
|
||||
|
||||
## FINDING
|
||||
|
||||
无 P0/P1 新 FINDING。
|
||||
|
||||
可选后续(P2/UX):rename 前端补 `validatePathSegment`;`sanitizeRemotePath` 收紧或 cd 改用引号包裹。
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] len(H) == Closure
|
||||
- [x] 五文件全文 Read(大文件分段至 EOF)
|
||||
- [x] 无 v-html / 凭据明文存储
|
||||
|
||||
## 验证
|
||||
|
||||
`bash scripts/local_verify.sh` — 全绿
|
||||
@@ -0,0 +1,76 @@
|
||||
# Phase 1 Delta 审计 — Wave D6(认证链 / Login)
|
||||
|
||||
**日期**: 2026-06-04
|
||||
**范围**: 登录页 + auth store + HTTP 客户端 + 路由守卫 + WS URL
|
||||
**标准**: `standards/line-walk-audit-standard-v2.md` 8 步
|
||||
|
||||
## 登记
|
||||
|
||||
| # | 文件 | 语言 | 行数 N | Read 范围 |
|
||||
|---|------|------|--------|-----------|
|
||||
| 1 | `frontend/src/pages/LoginPage.vue` | Vue | 244 | 1–244 全文 |
|
||||
| 2 | `frontend/src/stores/auth.ts` | TS | 132 | 1–132 全文 |
|
||||
| 3 | `frontend/src/api/index.ts` | TS | 308 | 1–308 全文 |
|
||||
| 4 | `frontend/src/router/index.ts` | TS | 68 | 1–68 全文 |
|
||||
| 5 | `frontend/src/utils/wsUrl.ts` | TS | 23 | 1–23 全文 |
|
||||
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
|
||||
| 文件 | H 命中 |
|
||||
|------|--------|
|
||||
| LoginPage.vue | H-Secret, H-Redirect, H-Lockout, H-XSS |
|
||||
| auth.ts | H-Token, H-Refresh, H-Profile |
|
||||
| api/index.ts | H-Auth, H-Refresh, H-401 |
|
||||
| router/index.ts | H-Guard, H-Redirect |
|
||||
| wsUrl.ts | H-WS |
|
||||
|
||||
## 入口表
|
||||
|
||||
| 模块 | 入口 | 鉴权 |
|
||||
|------|------|------|
|
||||
| LoginPage | POST `/auth/login` | 公开 |
|
||||
| LoginPage | GET `/settings/bing-wallpapers` | 公开(auth_jwt 白名单) |
|
||||
| auth store | POST `/auth/refresh`, `/auth/logout`, GET `/auth/me` | refresh cookie / Bearer |
|
||||
| api | `fetchAuthed` 全站 API | Bearer + credentials |
|
||||
| router | `beforeEach` | bootstrapped + isLoggedIn |
|
||||
| wsUrl | WS URL 构建 | 供 terminal/sync 使用 |
|
||||
|
||||
## Closure 表
|
||||
|
||||
| H | 文件:行号 | 判定 | 理由 |
|
||||
|---|-----------|------|------|
|
||||
| H-Secret | LoginPage.vue:102-103 | SAFE | password 仅组件 ref,不落 localStorage |
|
||||
| H-Secret | auth.ts:26-27 | SAFE | access token 内存 Pinia ref |
|
||||
| H-Redirect | LoginPage.vue:178-179 | SAFE | `startsWith('/') && !startsWith('//')` 防 open redirect |
|
||||
| H-Lockout | LoginPage.vue:186-188 | SAFE | 429 → 15min 本地 lockout UI |
|
||||
| H-TOTP | LoginPage.vue:182-184 | SAFE | TotpRequiredError → 显示 TOTP 字段 |
|
||||
| H-XSS | LoginPage.vue:11-12 | SAFE | `{{ error }}` 文本插值,无 v-html |
|
||||
| H-Refresh | auth.ts:45-66 | SAFE | HttpOnly cookie refresh;失败 silent |
|
||||
| H-Token | auth.ts:36-38 | SAFE | exp 解码仅用于续期阈值 |
|
||||
| H-Profile | auth.ts:89-97 | SAFE | 401 → forceLogout |
|
||||
| H-Auth | api/index.ts:68-70 | SAFE | Bearer 注入 |
|
||||
| H-Refresh | api/index.ts:19-44 | SAFE | 单飞 refreshInFlight 防并发 |
|
||||
| H-Refresh | api/index.ts:84-86 | SAFE | 过期前 5min 主动 refresh |
|
||||
| H-401 | api/index.ts:94-110 | SAFE | 401 重试一次后 forceLogout |
|
||||
| H-Cookie | api/index.ts:27-28,91 | SAFE | credentials: include |
|
||||
| H-Guard | router/index.ts:55-65 | SAFE | 非 public 路由需 isLoggedIn |
|
||||
| H-Guard | router/index.ts:57-58 | SAFE | bootstrapped 前 tryRestoreSession |
|
||||
| H-Public | router/index.ts:18 | SAFE | Login meta.public |
|
||||
| H-WS | wsUrl.ts:17-20 | RISK | token 放 query;已接受 |
|
||||
| H-Wallpaper | LoginPage.vue:121-123 | SAFE | 公开壁纸 API intentional |
|
||||
|
||||
**len(H)=19, Closure=19**
|
||||
|
||||
## FINDING
|
||||
|
||||
无 P0/P1 新 FINDING。
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] len(H) == Closure
|
||||
- [x] 五文件全文 Read
|
||||
- [x] 认证链:内存 token + HttpOnly refresh + 路由守卫
|
||||
|
||||
## 验证
|
||||
|
||||
`bash scripts/local_verify.sh` — 全绿
|
||||
@@ -0,0 +1,78 @@
|
||||
# Phase 1 Delta 审计 — Wave D7(Deploy / 本地验证脚本)
|
||||
|
||||
**日期**: 2026-06-04
|
||||
**范围**: 门控脚本 + 本地 dev/verify + docker-compose
|
||||
**标准**: 变更审查(运维/门控;非业务 API 8 步,沿用 Closure 表格式)
|
||||
|
||||
## 登记
|
||||
|
||||
| # | 文件 | 语言 | 行数 N | Read 范围 |
|
||||
|---|------|------|--------|-----------|
|
||||
| 1 | `deploy/pre_deploy_check.sh` | Bash | 299 | 1–299 全文 |
|
||||
| 2 | `scripts/local_verify.sh` | Bash | 25 | 1–25 全文 |
|
||||
| 3 | `scripts/local_integration_smoke.sh` | Bash | 91 | 1–91 全文 |
|
||||
| 4 | `scripts/start-dev.sh` | Bash | 90 | 1–90 全文 |
|
||||
| 5 | `docker-compose.yml` | YAML | 89 | 1–89 全文 |
|
||||
|
||||
## Step 3 — 规则扫描 H(摘要)
|
||||
|
||||
| 文件 | H 命中 |
|
||||
|------|--------|
|
||||
| pre_deploy_check.sh | H-Gate, H-Secret, H-Git |
|
||||
| local_verify.sh | H-Chain |
|
||||
| local_integration_smoke.sh | H-Health, H-Auth |
|
||||
| start-dev.sh | H-DevBind, H-Env |
|
||||
| docker-compose.yml | H-Port, H-DefaultCreds |
|
||||
|
||||
## 入口表(脚本调用链)
|
||||
|
||||
```
|
||||
local_verify.sh
|
||||
→ start-dev.sh (docker mysql+redis, uvicorn :8600)
|
||||
→ local_integration_smoke.sh (/health, JWT 401, unit tests)
|
||||
→ tests/test_api.py
|
||||
→ ruff check server/ -F
|
||||
|
||||
pre_deploy_check.sh (独立 L3 门控,7+2 pre-flight)
|
||||
```
|
||||
|
||||
## Closure 表
|
||||
|
||||
| H | 文件:行号 | 判定 | 理由 |
|
||||
|---|-----------|------|------|
|
||||
| H-Gate | pre_deploy_check.sh:50-74 | SAFE | Changelog 日期+≥10 行 |
|
||||
| H-Gate | pre_deploy_check.sh:76-105 | RISK | 审计路径 `docs/audit/`;delta 报告在 `docs/reports/` 不自动过 Gate2 |
|
||||
| H-Gate | pre_deploy_check.sh:107-131 | SAFE | test_api 必须存在且通过 |
|
||||
| H-Gate | pre_deploy_check.sh:134-160 | SAFE | ruff F;未装则 SKIP |
|
||||
| H-Gate | pre_deploy_check.sh:194-218 | RISK | bandit `-ll` 仅 HIGH;文档写 HIGH/MEDIUM 有漂移 |
|
||||
| H-Gate | pre_deploy_check.sh:221-258 | RISK | Gate7 用 `HEAD~1` diff;未 commit 改动可能漏检 |
|
||||
| H-Gate | pre_deploy_check.sh:261-284 | SAFE | shell/text EOL pre-flight |
|
||||
| H-Chain | local_verify.sh:10-22 | SAFE | 固定顺序 smoke→test_api→ruff |
|
||||
| H-Health | local_integration_smoke.sh:67-70 | SAFE | /health 200 |
|
||||
| H-Auth | local_integration_smoke.sh:72-75 | SAFE | /api/servers/ 无 JWT → 401 |
|
||||
| H-DevBind | start-dev.sh:75-76 | RISK | uvicorn `--host 0.0.0.0` 仅本地 dev |
|
||||
| H-Env | start-dev.sh:29-32 | SAFE | 无 .env 时 generate_env,不写 git |
|
||||
| H-Port | docker-compose.yml:40-41 | SAFE | Redis publish 6379 修复本机 uvicorn 连 Redis |
|
||||
| H-Port | docker-compose.yml:26-27 | RISK | MySQL 3306 暴露主机;dev 惯例 |
|
||||
| H-DefaultCreds | docker-compose.yml:14-17 | RISK | 默认 dev 密码;非生产 compose |
|
||||
| H-Secret | docker-compose.yml:68-70 | SAFE | SECRET/API/ENCRYPTION 从 env 注入,无硬编码 |
|
||||
|
||||
**len(H)=16, Closure=16**
|
||||
|
||||
## FINDING
|
||||
|
||||
无 P0/P1 新 FINDING。
|
||||
|
||||
**运维备注(非阻塞)**:
|
||||
- Phase1 delta 报告在 `docs/reports/audit-delta-*.md`;部署前若只跑 pre_deploy,需另备 `docs/audit/YYYY-MM-DD-*.md` 或调整 Gate2 路径。
|
||||
- 生产部署仍走 SSH + handoff,不依赖本 compose 默认密码。
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] len(H) == Closure
|
||||
- [x] 五文件全文 Read
|
||||
- [x] local_verify 链路可复现
|
||||
|
||||
## 验证
|
||||
|
||||
`bash scripts/local_verify.sh` — 全绿
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user