实现连接配置的导入/导出功能
This commit is contained in:
Generated
+204
-21
@@ -745,7 +745,6 @@
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
@@ -756,7 +755,6 @@
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -782,7 +780,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
|
||||
"integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
@@ -794,7 +791,6 @@
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
|
||||
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -817,21 +813,27 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/multer": {
|
||||
"version": "1.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
|
||||
"integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz",
|
||||
"integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@@ -841,21 +843,18 @@
|
||||
"version": "6.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
@@ -866,7 +865,6 @@
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
||||
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
@@ -1271,6 +1269,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||
@@ -1485,7 +1489,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buildcheck": {
|
||||
@@ -1497,6 +1500,17 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -1725,6 +1739,51 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
|
||||
"engines": [
|
||||
"node >= 0.8"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^2.2.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-stream/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
|
||||
@@ -1811,6 +1870,12 @@
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cpu-features": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||
@@ -2824,7 +2889,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
@@ -2935,6 +2999,12 @@
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -2961,8 +3031,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/klona": {
|
||||
"version": "2.0.6",
|
||||
@@ -3360,6 +3429,79 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "1.4.5-lts.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
||||
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^1.0.0",
|
||||
"concat-stream": "^1.5.2",
|
||||
"mkdirp": "^0.5.4",
|
||||
"object-assign": "^4.1.1",
|
||||
"type-is": "^1.6.4",
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.22.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz",
|
||||
@@ -3862,6 +4004,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/promise-inflight": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||
@@ -4398,7 +4546,6 @@
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
@@ -4409,7 +4556,6 @@
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
|
||||
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
@@ -4477,8 +4623,7 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sqlite3": {
|
||||
"version": "5.1.7",
|
||||
@@ -4555,6 +4700,14 @@
|
||||
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -4915,6 +5068,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
@@ -4972,7 +5131,6 @@
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
@@ -5394,7 +5552,6 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
@@ -5447,10 +5604,14 @@
|
||||
"name": "@nexus-terminal/backend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@types/multer": "^1.4.12",
|
||||
"bcrypt": "^5.1.1",
|
||||
"connect-sqlite3": "^0.9.15",
|
||||
"express": "^5.1.0",
|
||||
"express-session": "^1.18.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"socks": "^2.8.4",
|
||||
"sqlite3": "^5.1.7",
|
||||
"ssh2": "^1.16.0",
|
||||
"ws": "^8.18.1"
|
||||
@@ -5468,6 +5629,28 @@
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"packages/backend/node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"packages/backend/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"packages/frontend": {
|
||||
"name": "@nexus-terminal/frontend",
|
||||
"version": "0.1.0",
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
"dev": "npx ts-node-dev --respawn --transpile-only src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/multer": "^1.4.12",
|
||||
"bcrypt": "^5.1.1",
|
||||
"connect-sqlite3": "^0.9.15",
|
||||
"express": "^5.1.0",
|
||||
"express-session": "^1.18.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"socks": "^2.8.4",
|
||||
"sqlite3": "^5.1.7",
|
||||
"ssh2": "^1.16.0",
|
||||
|
||||
@@ -746,7 +746,349 @@ export const testConnection = async (req: Request, res: Response): Promise<void>
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`测试连接 ${connectionId} 时发生内部错误:`, error);
|
||||
res.status(500).json({ success: false, message: error.message || '测试连接时发生内部服务器错误。' });
|
||||
console.error(`测试连接 ${connectionId} 时发生内部错误:`, error);
|
||||
res.status(500).json({ success: false, message: error.message || '测试连接时发生内部服务器错误。' });
|
||||
}
|
||||
};
|
||||
|
||||
// --- 新增:导出连接配置 ---
|
||||
/**
|
||||
* 导出所有连接配置 (GET /api/v1/connections/export)
|
||||
*/
|
||||
export const exportConnections = async (req: Request, res: Response): Promise<void> => {
|
||||
const userId = req.session.userId; // 保留以备将来多用户
|
||||
|
||||
try {
|
||||
// 1. 查询所有连接及其关联的代理信息
|
||||
const connectionsWithProxies = await new Promise<any[]>((resolve, reject) => {
|
||||
db.all(
|
||||
`SELECT
|
||||
c.id, c.name, c.host, c.port, c.username, c.auth_method,
|
||||
c.encrypted_password, c.encrypted_private_key, c.encrypted_passphrase,
|
||||
c.proxy_id,
|
||||
p.id as proxy_db_id, p.name as proxy_name, p.type as proxy_type,
|
||||
p.host as proxy_host, p.port as proxy_port, p.username as proxy_username,
|
||||
-- p.auth_method as proxy_auth_method, -- Removed: Column likely doesn't exist based on error
|
||||
p.encrypted_password as proxy_encrypted_password
|
||||
-- p.encrypted_private_key as proxy_encrypted_private_key, -- Removed: Column likely doesn't exist
|
||||
-- p.encrypted_passphrase as proxy_encrypted_passphrase -- Removed: Column likely doesn't exist
|
||||
FROM connections c
|
||||
LEFT JOIN proxies p ON c.proxy_id = p.id
|
||||
ORDER BY c.name ASC`,
|
||||
(err, rows: any[]) => {
|
||||
if (err) {
|
||||
console.error('查询连接和代理信息以供导出时出错:', err.message);
|
||||
return reject(new Error('导出连接失败:查询连接信息出错'));
|
||||
}
|
||||
resolve(rows);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 2. 查询所有连接的标签信息
|
||||
const connectionTags = await new Promise<{[connId: number]: number[]}>((resolve, reject) => {
|
||||
db.all('SELECT connection_id, tag_id FROM connection_tags', (err, rows: {connection_id: number, tag_id: number}[]) => {
|
||||
if (err) {
|
||||
console.error('查询连接标签以供导出时出错:', err.message);
|
||||
return reject(new Error('导出连接失败:查询标签信息出错'));
|
||||
}
|
||||
const tagsMap: {[connId: number]: number[]} = {};
|
||||
rows.forEach(row => {
|
||||
if (!tagsMap[row.connection_id]) {
|
||||
tagsMap[row.connection_id] = [];
|
||||
}
|
||||
tagsMap[row.connection_id].push(row.tag_id);
|
||||
});
|
||||
resolve(tagsMap);
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 格式化数据以供导出
|
||||
const formattedData = connectionsWithProxies.map(row => {
|
||||
const connection: any = {
|
||||
// 不导出 id,因为导入时需要重新创建
|
||||
name: row.name,
|
||||
host: row.host,
|
||||
port: row.port,
|
||||
username: row.username,
|
||||
auth_method: row.auth_method,
|
||||
encrypted_password: row.encrypted_password,
|
||||
encrypted_private_key: row.encrypted_private_key,
|
||||
encrypted_passphrase: row.encrypted_passphrase,
|
||||
tag_ids: connectionTags[row.id] || [], // 从 map 中获取标签 ID
|
||||
// 不导出 created_at, updated_at, last_connected_at
|
||||
};
|
||||
|
||||
// 添加代理信息(如果存在)
|
||||
if (row.proxy_db_id) {
|
||||
connection.proxy = {
|
||||
// 不导出代理的 id,因为导入时可能需要重新创建或匹配
|
||||
name: row.proxy_name,
|
||||
type: row.proxy_type,
|
||||
host: row.proxy_host,
|
||||
port: row.proxy_port,
|
||||
username: row.proxy_username,
|
||||
// auth_method: row.proxy_auth_method, // Removed
|
||||
encrypted_password: row.proxy_encrypted_password
|
||||
// encrypted_private_key: row.proxy_encrypted_private_key, // Removed
|
||||
// encrypted_passphrase: row.proxy_encrypted_passphrase, // Removed
|
||||
};
|
||||
} else {
|
||||
connection.proxy = null; // 明确设为 null
|
||||
}
|
||||
|
||||
return connection;
|
||||
});
|
||||
|
||||
// 4. 设置响应头,提示浏览器下载文件
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `nexus-terminal-connections-${timestamp}.json`;
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
// 发送 JSON 数据
|
||||
res.status(200).json(formattedData);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('导出连接时发生错误:', error);
|
||||
res.status(500).json({ message: error.message || '导出连接时发生内部服务器错误。' });
|
||||
}
|
||||
};
|
||||
|
||||
// --- 新增:导入连接配置 ---
|
||||
/**
|
||||
* 导入连接配置 (POST /api/v1/connections/import)
|
||||
*/
|
||||
export const importConnections = async (req: Request, res: Response): Promise<void> => {
|
||||
const userId = req.session.userId; // 保留以备将来多用户
|
||||
|
||||
if (!req.file) {
|
||||
res.status(400).json({ message: '未找到上传的文件 (需要名为 "connectionsFile" 的文件)。' });
|
||||
return;
|
||||
}
|
||||
|
||||
let importedData: any[];
|
||||
try {
|
||||
const fileContent = req.file.buffer.toString('utf8');
|
||||
importedData = JSON.parse(fileContent);
|
||||
if (!Array.isArray(importedData)) {
|
||||
throw new Error('JSON 文件内容必须是一个数组。');
|
||||
}
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ message: `解析 JSON 文件失败: ${error.message}` });
|
||||
return;
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
const errors: { connectionName?: string; message: string }[] = [];
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// 准备数据库语句
|
||||
const findProxyStmt = db.prepare(`SELECT id FROM proxies WHERE name = ? AND type = ? AND host = ? AND port = ?`);
|
||||
// 恢复为文档定义的列
|
||||
const insertProxyStmt = db.prepare(`INSERT INTO proxies (name, type, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
const insertConnStmt = db.prepare(`INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
const insertTagStmt = db.prepare(`INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`);
|
||||
|
||||
try { // Wrap the entire database operation in a try block
|
||||
await new Promise<void>((resolveOuter, rejectOuter) => {
|
||||
// 使用事务处理导入
|
||||
db.serialize(() => { // Removed async here
|
||||
db.run('BEGIN TRANSACTION', async (beginErr: Error | null) => { // <--- Added async here
|
||||
if (beginErr) return rejectOuter(new Error(`开始事务失败: ${beginErr.message}`));
|
||||
|
||||
try {
|
||||
// 使用 Promise.allSettled 来处理所有连接的导入
|
||||
const importPromises = importedData.map(connData => (async () => { // async IIFE
|
||||
// 1. 验证基本连接数据结构
|
||||
if (!connData.name || !connData.host || !connData.port || !connData.username || !connData.auth_method) {
|
||||
// failureCount++; // 由 allSettled 结果判断
|
||||
// errors.push({ connectionName: connData.name || '未知连接', message: '缺少必要的连接字段 (name, host, port, username, auth_method)。' });
|
||||
throw new Error('缺少必要的连接字段 (name, host, port, username, auth_method)。');
|
||||
}
|
||||
// 验证 auth_method 和凭证
|
||||
if (connData.auth_method === 'password' && !connData.encrypted_password) {
|
||||
throw new Error('密码认证缺少 encrypted_password。');
|
||||
}
|
||||
if (connData.auth_method === 'key' && !connData.encrypted_private_key) {
|
||||
throw new Error('密钥认证缺少 encrypted_private_key。');
|
||||
}
|
||||
|
||||
let proxyIdToUse: number | null = null;
|
||||
|
||||
// 2. 处理代理信息(如果存在)
|
||||
if (connData.proxy) {
|
||||
const proxyData = connData.proxy;
|
||||
// 验证代理数据
|
||||
if (!proxyData.name || !proxyData.type || !proxyData.host || !proxyData.port /* || !proxyData.auth_method */) { // auth_method 可能不存在,暂时移除强制校验
|
||||
throw new Error('代理信息不完整 (缺少 name, type, host, port)。');
|
||||
}
|
||||
// 验证代理凭证存在性 (如果 auth_method 存在)
|
||||
if (proxyData.auth_method === 'password' && !proxyData.encrypted_password) {
|
||||
throw new Error('代理密码认证缺少 encrypted_password。');
|
||||
}
|
||||
if (proxyData.auth_method === 'key' && !proxyData.encrypted_private_key) {
|
||||
throw new Error('代理密钥认证缺少 encrypted_private_key。');
|
||||
}
|
||||
|
||||
// 尝试查找现有代理
|
||||
const existingProxy = await new Promise<{ id: number } | undefined>((resolve, reject) => {
|
||||
findProxyStmt.get(proxyData.name, proxyData.type, proxyData.host, proxyData.port, (err: Error | null, row: { id: number } | undefined) => {
|
||||
if (err) return reject(new Error(`查找代理时出错: ${err.message}`));
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
if (existingProxy) {
|
||||
proxyIdToUse = existingProxy.id;
|
||||
console.log(`导入连接 ${connData.name}: 找到现有代理 ${proxyData.name} (ID: ${proxyIdToUse})`);
|
||||
} else {
|
||||
// 代理不存在,创建新代理
|
||||
console.log(`导入连接 ${connData.name}: 代理 ${proxyData.name} 不存在,正在创建...`);
|
||||
const proxyResult = await new Promise<{ lastID: number }>((resolve, reject) => {
|
||||
insertProxyStmt.run(
|
||||
proxyData.name, proxyData.type, proxyData.host, proxyData.port,
|
||||
proxyData.username || null,
|
||||
// 恢复为文档定义的参数
|
||||
proxyData.auth_method || 'none', // 提供默认值 'none' 如果不存在
|
||||
proxyData.encrypted_password || null,
|
||||
proxyData.encrypted_private_key || null,
|
||||
proxyData.encrypted_passphrase || null,
|
||||
now, now,
|
||||
function (this: Statement, err: Error | null) {
|
||||
if (err) return reject(new Error(`创建代理时出错: ${err.message}`));
|
||||
resolve({ lastID: (this as any).lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
proxyIdToUse = proxyResult.lastID;
|
||||
console.log(`导入连接 ${connData.name}: 新代理 ${proxyData.name} 创建成功 (ID: ${proxyIdToUse})`);
|
||||
}
|
||||
} // 结束代理处理
|
||||
|
||||
// 3. 插入连接信息
|
||||
const connResult = await new Promise<{ lastID: number }>((resolve, reject) => {
|
||||
insertConnStmt.run(
|
||||
connData.name, connData.host, connData.port, connData.username, connData.auth_method,
|
||||
connData.encrypted_password || null,
|
||||
connData.encrypted_private_key || null,
|
||||
connData.encrypted_passphrase || null,
|
||||
proxyIdToUse, // 使用找到或创建的代理 ID
|
||||
now, now,
|
||||
function (this: Statement, err: Error | null) {
|
||||
if (err) return reject(new Error(`插入连接时出错: ${err.message}`));
|
||||
resolve({ lastID: (this as any).lastID });
|
||||
}
|
||||
);
|
||||
});
|
||||
const newConnectionId = connResult.lastID;
|
||||
|
||||
// 4. 处理标签关联
|
||||
if (Array.isArray(connData.tag_ids) && connData.tag_ids.length > 0) {
|
||||
for (const tagId of connData.tag_ids) {
|
||||
if (typeof tagId === 'number' && tagId > 0) {
|
||||
// 注意:这里假设 tagId 在 tags 表中已存在。
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
insertTagStmt.run(newConnectionId, tagId, (err: Error | null) => {
|
||||
if (err) {
|
||||
console.warn(`导入连接 ${connData.name}: 关联标签 ID ${tagId} 失败: ${err.message}`);
|
||||
// 决定是否因此失败
|
||||
// reject(new Error(`关联标签 ID ${tagId} 失败: ${err.message}`));
|
||||
}
|
||||
resolve(); // 继续处理下一个标签
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果 IIFE 成功完成,返回 null 或 undefined 表示成功
|
||||
return null;
|
||||
|
||||
})().catch(err => { // 捕获 async IIFE 中的错误
|
||||
// 返回一个包含错误信息的对象,以便 Promise.allSettled 处理
|
||||
return { connectionName: connData.name || '未知连接', error: err };
|
||||
})); // 结束 map 和 async IIFE
|
||||
|
||||
// 等待所有导入 Promise 完成
|
||||
const results = await Promise.allSettled(importPromises);
|
||||
|
||||
// 处理结果
|
||||
results.forEach(result => {
|
||||
if (result.status === 'fulfilled' && result.value?.error) {
|
||||
// IIFE 成功执行但内部捕获并返回了错误
|
||||
failureCount++;
|
||||
errors.push({ connectionName: result.value.connectionName, message: result.value.error.message });
|
||||
} else if (result.status === 'fulfilled') {
|
||||
// IIFE 成功执行且没有返回错误
|
||||
successCount++;
|
||||
} else { // status === 'rejected' - IIFE 本身抛出未捕获错误
|
||||
failureCount++;
|
||||
const reason = result.reason as any;
|
||||
// 尝试获取 connectionName,如果 IIFE 在早期失败可能没有
|
||||
const name = importedData[results.indexOf(result)]?.name || '未知连接';
|
||||
errors.push({ connectionName: name, message: reason?.message || '未知导入错误' });
|
||||
}
|
||||
});
|
||||
|
||||
// 根据是否有失败决定提交或回滚
|
||||
if (failureCount > 0) {
|
||||
console.warn(`导入连接存在 ${failureCount} 个错误,正在回滚事务...`);
|
||||
db.run('ROLLBACK', (rollbackErr: Error | null) => {
|
||||
if (rollbackErr) console.error("回滚事务失败:", rollbackErr);
|
||||
// 即使回滚失败,仍需告知前端导入失败
|
||||
rejectOuter(new Error(`导入失败,存在 ${failureCount} 个错误。`)); // 使用 rejectOuter 传递错误
|
||||
});
|
||||
} else {
|
||||
// 所有记录处理完毕,提交事务
|
||||
db.run('COMMIT', (commitErr: Error | null) => {
|
||||
if (commitErr) {
|
||||
console.error('提交导入事务时出错:', commitErr);
|
||||
rejectOuter(new Error(`提交导入事务失败: ${commitErr.message}`));
|
||||
} else {
|
||||
resolveOuter(); // 事务成功,resolve 外层 Promise
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (innerError: any) {
|
||||
// 捕获 Promise.allSettled 或其他同步错误
|
||||
console.error('导入事务内部出错:', innerError);
|
||||
db.run('ROLLBACK', (rollbackErr: Error | null) => {
|
||||
if (rollbackErr) console.error("回滚事务失败:", rollbackErr);
|
||||
rejectOuter(innerError); // 将内部错误传递出去
|
||||
});
|
||||
}
|
||||
}); // 结束 BEGIN TRANSACTION 回调
|
||||
}); // 结束 db.serialize
|
||||
}); // 结束 new Promise
|
||||
|
||||
// 如果 Promise 成功 resolve (事务提交成功)
|
||||
res.status(200).json({
|
||||
message: `导入成功完成。共导入 ${successCount} 条连接。`,
|
||||
successCount,
|
||||
failureCount: 0
|
||||
});
|
||||
|
||||
} catch (error: any) { // 捕获外层 try 或 rejectOuter 传递的错误
|
||||
console.error('导入连接时发生错误:', error);
|
||||
// 如果错误是由 rejectOuter 传递的,并且包含失败计数,则使用它
|
||||
if (failureCount > 0) {
|
||||
res.status(400).json({
|
||||
message: error.message || `导入失败,存在 ${failureCount} 个错误。`,
|
||||
successCount,
|
||||
failureCount,
|
||||
errors
|
||||
});
|
||||
} else {
|
||||
// 其他错误 (如文件解析、开始事务失败等)
|
||||
res.status(500).json({ message: error.message || '导入连接时发生内部服务器错误。' });
|
||||
}
|
||||
} finally {
|
||||
// Finalize prepared statements regardless of success or failure
|
||||
// Ensure statements are finalized even if db.serialize wasn't fully entered
|
||||
findProxyStmt?.finalize();
|
||||
insertProxyStmt?.finalize();
|
||||
insertConnStmt?.finalize();
|
||||
insertTagStmt?.finalize();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,19 +1,68 @@
|
||||
import { Router } from 'express';
|
||||
import { Router, Request, Response, NextFunction } from 'express'; // 引入 Request, Response, NextFunction
|
||||
import { isAuthenticated } from '../auth/auth.middleware'; // 引入认证中间件
|
||||
import multer from 'multer'; // 引入 multer 用于文件上传
|
||||
import {
|
||||
createConnection,
|
||||
getConnections,
|
||||
getConnectionById, // 引入获取单个连接的控制器
|
||||
updateConnection, // 引入更新连接的控制器
|
||||
deleteConnection, // 引入删除连接的控制器
|
||||
testConnection // 引入测试连接的控制器
|
||||
testConnection, // 引入测试连接的控制器
|
||||
exportConnections, // 引入导出连接的控制器
|
||||
importConnections // 引入导入连接的控制器
|
||||
} from './connections.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 配置 multer 用于处理 JSON 文件上传 (存储在内存中)
|
||||
const storage = multer.memoryStorage(); // 将文件存储在内存中作为 Buffer
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 限制文件大小为 5MB
|
||||
fileFilter: (req: Request, file, cb) => { // Add type for req
|
||||
if (file.mimetype === 'application/json') {
|
||||
cb(null, true);
|
||||
} else {
|
||||
// Attach error to request instead of calling cb with error directly
|
||||
// This makes it easier to handle consistently and return JSON
|
||||
(req as any).fileValidationError = '只允许上传 JSON 文件!';
|
||||
cb(null, false); // Reject the file
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 应用认证中间件到所有 /connections 路由
|
||||
router.use(isAuthenticated); // 恢复认证检查
|
||||
|
||||
// --- Specific routes before parameterized routes ---
|
||||
|
||||
// GET /api/v1/connections/export - 导出连接配置
|
||||
router.get('/export', exportConnections);
|
||||
|
||||
// POST /api/v1/connections/import - 导入连接配置
|
||||
router.post('/import', (req: Request, res: Response, next: NextFunction) => {
|
||||
// Use multer middleware, but handle errors specifically
|
||||
upload.single('connectionsFile')(req, res, (err: any) => {
|
||||
// Check for file filter validation error first
|
||||
if ((req as any).fileValidationError) {
|
||||
return res.status(400).json({ message: (req as any).fileValidationError });
|
||||
}
|
||||
// Check for other multer errors (e.g., file size limit)
|
||||
if (err instanceof multer.MulterError) {
|
||||
return res.status(400).json({ message: `文件上传错误: ${err.message}` });
|
||||
} else if (err) {
|
||||
// Other unexpected errors during upload
|
||||
console.error("Unexpected error during file upload:", err);
|
||||
return res.status(500).json({ message: '文件上传处理失败' });
|
||||
}
|
||||
// If no errors, proceed to the controller
|
||||
next();
|
||||
});
|
||||
}, importConnections);
|
||||
|
||||
|
||||
// --- General CRUD and other routes ---
|
||||
|
||||
// GET /api/v1/connections - 获取连接列表
|
||||
router.get('/', getConnections);
|
||||
|
||||
|
||||
Binary file not shown.
@@ -86,7 +86,15 @@
|
||||
"test": {
|
||||
"success": "Connection test successful!",
|
||||
"failed": "Connection test failed: {error}"
|
||||
}
|
||||
},
|
||||
"exportConnections": "Export Connections",
|
||||
"importConnections": "Import Connections",
|
||||
"exportError": "Failed to export connections: {message}",
|
||||
"importError": "Failed to import connections: {message}",
|
||||
"importErrorFileType": "Invalid file type. Please select a JSON file.",
|
||||
"importErrorUnknown": "Unknown import error occurred.",
|
||||
"importErrorNetwork": "Network error during import.",
|
||||
"importSuccess": "Import completed. Successful: {successCount}, Failed: {failureCount}."
|
||||
},
|
||||
"proxies": {
|
||||
"title": "Proxy Management",
|
||||
|
||||
@@ -86,7 +86,15 @@
|
||||
"test": {
|
||||
"success": "连接测试成功!",
|
||||
"failed": "连接测试失败: {error}"
|
||||
}
|
||||
},
|
||||
"exportConnections": "导出连接",
|
||||
"importConnections": "导入连接",
|
||||
"exportError": "导出连接失败: {message}",
|
||||
"importError": "导入连接失败: {message}",
|
||||
"importErrorFileType": "文件类型无效。请选择 JSON 文件。",
|
||||
"importErrorUnknown": "发生未知导入错误。",
|
||||
"importErrorNetwork": "导入过程中发生网络错误。",
|
||||
"importSuccess": "导入完成。成功: {successCount}, 失败: {failureCount}."
|
||||
},
|
||||
"proxies": {
|
||||
"title": "代理管理",
|
||||
|
||||
@@ -62,6 +62,143 @@ const closeForm = () => {
|
||||
editingConnection.value = null; // 清除编辑状态
|
||||
showForm.value = false;
|
||||
};
|
||||
|
||||
// 新增:处理导出连接按钮点击事件
|
||||
const handleExportConnections = async () => {
|
||||
try {
|
||||
// 调用后端导出 API
|
||||
const response = await fetch('/api/v1/connections/export', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
// 如果需要认证,可能需要添加 Authorization 头
|
||||
// 'Authorization': `Bearer ${token}`, // 示例
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// 处理错误响应
|
||||
const errorData = await response.json();
|
||||
console.error('导出连接失败:', errorData.message || response.statusText);
|
||||
alert(t('connections.exportError', { message: errorData.message || response.statusText })); // 提示用户错误
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取文件名 (从 Content-Disposition 头)
|
||||
const disposition = response.headers.get('Content-Disposition');
|
||||
let filename = 'nexus-terminal-connections.json'; // 默认文件名
|
||||
if (disposition && disposition.includes('attachment')) {
|
||||
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
|
||||
const matches = filenameRegex.exec(disposition);
|
||||
if (matches != null && matches[1]) {
|
||||
filename = matches[1].replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 Blob 对象并触发下载
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a); // 需要添加到 DOM 才能触发点击
|
||||
a.click();
|
||||
a.remove(); // 清理
|
||||
window.URL.revokeObjectURL(url); // 释放对象 URL
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('导出连接时发生网络或处理错误:', error);
|
||||
alert(t('connections.exportError', { message: error.message || '未知错误' }));
|
||||
}
|
||||
};
|
||||
|
||||
// --- Import/Export Logic ---
|
||||
const fileInput = ref<HTMLInputElement | null>(null); // Ref for the hidden file input
|
||||
|
||||
// 点击导入按钮时触发文件选择
|
||||
const handleImportClick = () => {
|
||||
fileInput.value?.click(); // 触发隐藏 input 的点击事件
|
||||
};
|
||||
|
||||
// 处理文件选择变化
|
||||
const handleFileChange = async (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
|
||||
if (!file) {
|
||||
return; // 没有选择文件
|
||||
}
|
||||
|
||||
if (file.type !== 'application/json') {
|
||||
alert(t('connections.importErrorFileType')); // 提示文件类型错误
|
||||
target.value = ''; // 清空文件输入,以便可以再次选择相同文件
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('connectionsFile', file); // 'connectionsFile' 必须与后端 multer 配置匹配
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/connections/import', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
// 不需要 'Content-Type': 'multipart/form-data',浏览器会自动设置
|
||||
// 如果需要认证,添加 Authorization 头
|
||||
'Accept': 'application/json', // 期望服务器返回 JSON
|
||||
},
|
||||
});
|
||||
|
||||
// 首先检查响应是否成功
|
||||
if (!response.ok) {
|
||||
let errorMsg = `${response.status} ${response.statusText}`;
|
||||
try {
|
||||
// 尝试解析可能的 JSON 错误体
|
||||
const errorResult = await response.json();
|
||||
console.error('导入连接失败 (JSON):', errorResult.message || response.statusText, errorResult.errors);
|
||||
errorMsg = errorResult.message || errorMsg;
|
||||
if (errorResult.errors && Array.isArray(errorResult.errors)) {
|
||||
errorMsg += '\n' + errorResult.errors.map((e: any) => `- ${e.connectionName || '未知'}: ${e.message}`).join('\n');
|
||||
}
|
||||
} catch (jsonError) {
|
||||
// 如果解析 JSON 失败,尝试读取文本
|
||||
try {
|
||||
const textError = await response.text();
|
||||
console.error('导入连接失败 (Text):', textError);
|
||||
// 如果文本错误信息不为空,则使用它,否则保留状态码/文本
|
||||
if (textError) {
|
||||
errorMsg = textError.substring(0, 500); // 限制长度避免过长的 HTML 错误页
|
||||
}
|
||||
} catch (textReadError) {
|
||||
console.error('读取错误响应文本失败:', textReadError);
|
||||
// 保留原始的状态码/文本错误
|
||||
}
|
||||
}
|
||||
alert(t('connections.importError', { message: errorMsg }));
|
||||
} else {
|
||||
// 响应成功,解析 JSON
|
||||
try {
|
||||
const result = await response.json();
|
||||
console.log('导入连接成功:', result);
|
||||
alert(t('connections.importSuccess', { successCount: result.successCount, failureCount: result.failureCount }));
|
||||
// 导入成功后刷新连接列表
|
||||
connectionsStore.fetchConnections();
|
||||
} catch (jsonParseError: any) {
|
||||
console.error('解析成功响应 JSON 时出错:', jsonParseError);
|
||||
alert(t('connections.importError', { message: `无法解析服务器成功响应: ${jsonParseError.message}` }));
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('导入连接时发生网络或处理错误:', error);
|
||||
alert(t('connections.importError', { message: error.message || t('connections.importErrorNetwork') }));
|
||||
} finally {
|
||||
// 无论成功或失败,都清空文件输入框的值,以便用户可以重新上传相同的文件
|
||||
if (target) {
|
||||
target.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -69,7 +206,12 @@ const closeForm = () => {
|
||||
<h2>{{ t('connections.title') }}</h2>
|
||||
|
||||
<div class="actions-bar">
|
||||
<button @click="openAddForm" v-if="!showForm">{{ t('connections.addConnection') }}</button>
|
||||
<div> <!-- Wrap buttons -->
|
||||
<button @click="openAddForm" v-if="!showForm" style="margin-right: 0.5rem;">{{ t('connections.addConnection') }}</button>
|
||||
<button @click="handleExportConnections" style="margin-right: 0.5rem;">{{ t('connections.exportConnections') }}</button> <!-- Export Button -->
|
||||
<button @click="handleImportClick">{{ t('connections.importConnections') }}</button> <!-- Import Button -->
|
||||
<input type="file" ref="fileInput" @change="handleFileChange" accept=".json" style="display: none;" /> <!-- Hidden File Input -->
|
||||
</div>
|
||||
<!-- 标签筛选下拉框 -->
|
||||
<select v-model="selectedTagId" class="tag-filter-select">
|
||||
<option :value="null">{{ t('connections.filterAllTags') }}</option>
|
||||
|
||||
Reference in New Issue
Block a user