diff --git a/package-lock.json b/package-lock.json index 6764b73..7a9359f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,20 @@ "hasInstallScript": true, "dependencies": { "@element-plus/icons-vue": "^2.3.1", + "@lydell/node-pty": "^1.2.0-beta.12", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", "better-sqlite3": "^11.5.0", "electron-store": "^8.2.0", "element-plus": "^2.8.4", "highlight.js": "^11.10.0", "markdown-it": "^14.3.0", + "node-hid": "^3.3.0", "pinia": "^2.2.4", + "serialport": "^13.0.0", + "sudo-prompt": "^9.2.1", + "usb": "^3.0.1", "ws": "^8.18.0" }, "devDependencies": { @@ -1174,6 +1182,98 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lydell/node-pty": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmmirror.com/@lydell/node-pty/-/node-pty-1.2.0-beta.12.tgz", + "integrity": "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g==", + "license": "MIT", + "optionalDependencies": { + "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", + "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", + "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", + "@lydell/node-pty-linux-x64": "1.2.0-beta.12", + "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", + "@lydell/node-pty-win32-x64": "1.2.0-beta.12" + } + }, + "node_modules/@lydell/node-pty-darwin-arm64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmmirror.com/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.2.0-beta.12.tgz", + "integrity": "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lydell/node-pty-darwin-x64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmmirror.com/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.2.0-beta.12.tgz", + "integrity": "sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lydell/node-pty-linux-arm64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmmirror.com/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.2.0-beta.12.tgz", + "integrity": "sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lydell/node-pty-linux-x64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmmirror.com/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.2.0-beta.12.tgz", + "integrity": "sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lydell/node-pty-win32-arm64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmmirror.com/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.2.0-beta.12.tgz", + "integrity": "sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lydell/node-pty-win32-x64": { + "version": "1.2.0-beta.12", + "resolved": "https://registry.npmmirror.com/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.2.0-beta.12.tgz", + "integrity": "sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -1252,6 +1352,166 @@ "node": ">= 10.0.0" } }, + "node_modules/@node-usb/usb-darwin-arm64": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-darwin-arm64/-/usb-darwin-arm64-3.0.1.tgz", + "integrity": "sha512-GB+MGKaXvEtaDOWng7nCPxWvnXWhZtbFL0FW9ncDmGYtFl3OW+xfLZGhd5Y9VKG6GXXIYS3x1FsBI+zkcxVMhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@node-usb/usb-darwin-x64": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-darwin-x64/-/usb-darwin-x64-3.0.1.tgz", + "integrity": "sha512-oTY8vLoigjYFwNft1nZNNje0zAXjSQy9u3swe+pcCfZp6qQbEoSLegLEBr94arEgQloQabaq4VA2JZVA8cUQqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@node-usb/usb-linux-arm-gnueabihf": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-linux-arm-gnueabihf/-/usb-linux-arm-gnueabihf-3.0.1.tgz", + "integrity": "sha512-GN5dTxc/FhjmC9NP7Xx1esGAhpIhnNvAf02kk/0i7Cz0w1VaJ/I0XbIVAYIJ5zhDg/oSYkhdILHbOS9+mlWCRQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@node-usb/usb-linux-arm64-gnu": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-linux-arm64-gnu/-/usb-linux-arm64-gnu-3.0.1.tgz", + "integrity": "sha512-7cL85qb/yrBXvssZwL3k62duBxT/LaCY/Vcfv/Du9MLFdHhqU0zdYX5fLch/zaW5euYXQE0gmCoTfimfAjlxGQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@node-usb/usb-linux-arm64-musl": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-linux-arm64-musl/-/usb-linux-arm64-musl-3.0.1.tgz", + "integrity": "sha512-EweA4HeTwzLiRHsU2DRFflJ3vx0shB5Z9Lt5/P/bkoUtSNt8uGjFCmsCjqoO068ul9oejsBMJg22rNLD3+iFqQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@node-usb/usb-linux-x64-gnu": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-linux-x64-gnu/-/usb-linux-x64-gnu-3.0.1.tgz", + "integrity": "sha512-cEE7GyMhtfq5OM/MXZn3RmYBqPvId0CMuoBx7biazPvlnf0iEWYjpma0WvVMWad/f1QIcC45WgrazHaVv6gAPw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@node-usb/usb-linux-x64-musl": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-linux-x64-musl/-/usb-linux-x64-musl-3.0.1.tgz", + "integrity": "sha512-zGWlk1mzElTPO4wxlRcd1C7xd7xDxzgpOXbQfE+NbClOUJ71NkbnahWN2ef2LcJvFESYg/CV9Dv/ULKSpJWEEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@node-usb/usb-win32-arm64-msvc": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-win32-arm64-msvc/-/usb-win32-arm64-msvc-3.0.1.tgz", + "integrity": "sha512-ayXNeb+KvdkZLXzTWyttNONUPG8/x9PwuP+GvUeiYCQuyLMqmbGeMsO7IiuBWTnxLFtCdep+UBpcL8XMcYdTiA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@node-usb/usb-win32-ia32-msvc": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-win32-ia32-msvc/-/usb-win32-ia32-msvc-3.0.1.tgz", + "integrity": "sha512-U5281i6yY7gtoTwLom8nLF2o+ILI/g4OFpbAJBr4GbVPOTu+CSNF2Ea+dLEAG30148mp8v79NEsIiAbWXS/1aw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@node-usb/usb-win32-x64-msvc": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@node-usb/usb-win32-x64-msvc/-/usb-win32-x64-msvc-3.0.1.tgz", + "integrity": "sha512-T1jzCBwgnA6E5otxJS/j44b//6fGzHSDmLQtNN1fX6JvdQIisgtbf2+iKD/YI7E247j6Yi9Ixt+MKjHt64hugA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1624,6 +1884,254 @@ "win32" ] }, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz", + "integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "12.0.0", + "debug": "4.4.0", + "node-addon-api": "8.3.0", + "node-gyp-build": "4.8.4" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", + "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", + "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", + "license": "MIT", + "dependencies": { + "@serialport/parser-delimiter": "12.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/node-addon-api": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-8.3.0.tgz", + "integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", + "license": "MIT", + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz", + "integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz", + "integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz", + "integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz", + "integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz", + "integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==", + "license": "MIT", + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-readline/-/parser-readline-13.0.0.tgz", + "integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==", + "license": "MIT", + "dependencies": { + "@serialport/parser-delimiter": "13.0.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-ready/-/parser-ready-13.0.0.tgz", + "integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-regex/-/parser-regex-13.0.0.tgz", + "integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz", + "integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz", + "integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/@serialport/stream/-/stream-13.0.0.tgz", + "integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==", + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-4.6.0.tgz", @@ -1814,6 +2322,12 @@ "license": "MIT", "optional": true }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.14", + "resolved": "https://registry.npmmirror.com/@types/w3c-web-usb/-/w3c-web-usb-1.0.14.tgz", + "integrity": "sha512-Qu3Nn6JFuF4+sHKYl+IcX9vYiI40ogleXzFFSxoE1W94rG98o/kXs8uJ0QSfFzuwBCZWlGfUGpPkgwuuX4PchA==", + "license": "MIT" + }, "node_modules/@types/web-bluetooth": { "version": "0.0.16", "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", @@ -2140,6 +2654,27 @@ "node": ">=14.6" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/addon-web-links": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz", + "integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, "node_modules/7zip-bin": { "version": "5.2.0", "resolved": "https://registry.npmmirror.com/7zip-bin/-/7zip-bin-5.2.0.tgz", @@ -3277,7 +3812,6 @@ "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5338,7 +5872,6 @@ "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/muggle-string": { @@ -5404,6 +5937,40 @@ "license": "MIT", "optional": true }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-hid": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/node-hid/-/node-hid-3.3.0.tgz", + "integrity": "sha512-j+dFgJLRAE0nufQKXk3IfS6T6YuHhCgMvz4TrG0sgtb6DSCdYpfJ1etcdmeCmPQjUgO+yo32ktVrRliNs/+fmg==", + "hasInstallScript": true, + "license": "(MIT OR X11)", + "dependencies": { + "node-addon-api": "^3.2.1", + "pkg-prebuilds": "^1.0.0" + }, + "bin": { + "hid-showdevices": "src/show-devices.js" + }, + "engines": { + "node": ">=10.16" + } + }, + "node_modules/node-hid/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.51", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.51.tgz", @@ -5666,6 +6233,19 @@ } } }, + "node_modules/pkg-prebuilds": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/pkg-prebuilds/-/pkg-prebuilds-1.1.0.tgz", + "integrity": "sha512-jyai+KTQ2OwbN6iRYw88XbYOMgtpoSYJpjYebx7d9ihqz3txNi3ucsBt3va0iVWe6svSlaqpijMHFF/eJCMZzg==", + "license": "MIT", + "bin": { + "pkg-prebuilds-copy": "bin/copy.mjs", + "pkg-prebuilds-verify": "bin/verify.mjs" + }, + "engines": { + "node": ">= 14.15.0" + } + }, "node_modules/pkg-up": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/pkg-up/-/pkg-up-3.1.0.tgz", @@ -6088,6 +6668,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serialport": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/serialport/-/serialport-13.0.0.tgz", + "integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==", + "license": "MIT", + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "13.0.0", + "@serialport/parser-byte-length": "13.0.0", + "@serialport/parser-cctalk": "13.0.0", + "@serialport/parser-delimiter": "13.0.0", + "@serialport/parser-inter-byte-timeout": "13.0.0", + "@serialport/parser-packet-length": "13.0.0", + "@serialport/parser-readline": "13.0.0", + "@serialport/parser-ready": "13.0.0", + "@serialport/parser-regex": "13.0.0", + "@serialport/parser-slip-encoder": "13.0.0", + "@serialport/parser-spacepacket": "13.0.0", + "@serialport/stream": "13.0.0", + "debug": "4.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/serialport/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6347,6 +6972,13 @@ "node": ">=0.10.0" } }, + "node_modules/sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmmirror.com/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz", @@ -6613,6 +7245,30 @@ "punycode": "^2.1.0" } }, + "node_modules/usb": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/usb/-/usb-3.0.1.tgz", + "integrity": "sha512-IQIi5EraqmKssMNcXe7afKhriHFAE5WdfykhvgkOschJyHBkJXtk/PDx/DzVNyfI7qdr+BUbmLziXA+I3z02lw==", + "license": "MIT", + "dependencies": { + "@types/w3c-web-usb": "^1.0.14" + }, + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + }, + "optionalDependencies": { + "@node-usb/usb-darwin-arm64": "3.0.1", + "@node-usb/usb-darwin-x64": "3.0.1", + "@node-usb/usb-linux-arm-gnueabihf": "3.0.1", + "@node-usb/usb-linux-arm64-gnu": "3.0.1", + "@node-usb/usb-linux-arm64-musl": "3.0.1", + "@node-usb/usb-linux-x64-gnu": "3.0.1", + "@node-usb/usb-linux-x64-musl": "3.0.1", + "@node-usb/usb-win32-arm64-msvc": "3.0.1", + "@node-usb/usb-win32-ia32-msvc": "3.0.1", + "@node-usb/usb-win32-x64-msvc": "3.0.1" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", diff --git a/package.json b/package.json index 95c6d21..2812d3a 100644 --- a/package.json +++ b/package.json @@ -9,26 +9,31 @@ "build": "electron-vite build", "preview": "electron-vite preview", "start": "electron-vite preview", - "typecheck:node": "tsc --noEmit -p tsconfig.node.json", "typecheck:web": "vue-tsc --noEmit -p src/renderer/tsconfig.json", "typecheck": "npm run typecheck:node && npm run typecheck:web", - "package:win": "cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ electron-vite build && electron-builder --win --x64", "package:mac": "cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ electron-vite build && electron-builder --mac", "package:linux": "cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ electron-vite build && electron-builder --linux", "package:dir": "cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ electron-vite build && electron-builder --dir", - "postinstall": "electron-builder install-app-deps" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", + "@lydell/node-pty": "^1.2.0-beta.12", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", "better-sqlite3": "^11.5.0", "electron-store": "^8.2.0", "element-plus": "^2.8.4", "highlight.js": "^11.10.0", "markdown-it": "^14.3.0", + "node-hid": "^3.3.0", "pinia": "^2.2.4", + "serialport": "^13.0.0", + "sudo-prompt": "^9.2.1", + "usb": "^3.0.1", "ws": "^8.18.0" }, "devDependencies": { @@ -59,6 +64,14 @@ "out/**/*", "package.json" ], + "asarUnpack": [ + "**/node_modules/@lydell/node-pty/**/*", + "**/node_modules/@lydell/node-pty-*/*", + "**/node_modules/@node-usb/**/*", + "**/node_modules/usb/prebuilds/**/*", + "**/node_modules/@serialport/**/prebuilds/**/*", + "**/node_modules/node-hid/prebuilds/**/*" + ], "extraResources": [ { "from": "resources/icon.png", @@ -68,7 +81,12 @@ "win": { "icon": "resources/icon.png", "target": [ - { "target": "nsis", "arch": ["x64"] } + { + "target": "nsis", + "arch": [ + "x64" + ] + } ], "artifactName": "${productName}-${version}-Setup.${ext}" }, @@ -88,7 +106,9 @@ }, "linux": { "icon": "resources/icon.png", - "target": ["AppImage"] + "target": [ + "AppImage" + ] } } } diff --git a/src/main/chat-server.ts b/src/main/chat-server.ts index 445b778..e476571 100644 --- a/src/main/chat-server.ts +++ b/src/main/chat-server.ts @@ -11,6 +11,8 @@ export interface ChatServerEvents { recall: (from: DeviceInfo, messageId: string) => void typing: (from: DeviceInfo) => void read: (from: DeviceInfo, upToMessageId: string) => void + // 任意 ws 帧 (含 terminal/forward/usb 等), 让上层路由分发 + frame: (from: DeviceInfo, frame: WsFrame) => void } export class ChatServer extends EventEmitter { @@ -52,6 +54,8 @@ export class ChatServer extends EventEmitter { } catch { return } + // 通用: 任何帧都抛 'frame', 上层 (ipc.bindNetworkContext) 自行路由 + if (peer) this.emit('frame', peer, frame) switch (frame.type) { case 'hello': if (frame.from && frame.from.deviceId) { @@ -79,12 +83,12 @@ export class ChatServer extends EventEmitter { if (peer) this.emit('recall', peer, frame.messageId) break case 'read': - // 已读回执: 接收方已看到 upTo (含), 把 ≤ upTo 的发送方消息视为已读 if (peer) this.emit('read', peer, frame.upTo) break case 'typing': if (peer) this.emit('typing', peer) break + // terminal / forward / usb 由 'frame' 事件统一交给上层处理 } }) diff --git a/src/main/db.ts b/src/main/db.ts index f834970..228cf81 100644 --- a/src/main/db.ts +++ b/src/main/db.ts @@ -42,6 +42,23 @@ CREATE TABLE IF NOT EXISTS unread ( device_id TEXT PRIMARY KEY, count INTEGER NOT NULL DEFAULT 0 ); + +-- 远程操作审计 (terminal / forward / usb) +CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, + action TEXT NOT NULL, + source_device TEXT, + target_device TEXT, + session_id TEXT, + payload_json TEXT, + result TEXT NOT NULL, + bytes_in INTEGER NOT NULL DEFAULT 0, + bytes_out INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(ts DESC); +CREATE INDEX IF NOT EXISTS idx_audit_session ON audit_log(session_id); `) // 迁移: 给老库补 ignored 列 (用 try/catch 包住, 已存在会抛) @@ -240,3 +257,72 @@ export function totalUnread(): number { const r = db.prepare(`SELECT COALESCE(SUM(count), 0) AS s FROM unread`).get() as { s: number } return r.s } + +// 审计日志 (远程操作: terminal / forward / usb) +export interface AuditRow { + id: number + ts: number + action: string + source_device: string | null + target_device: string | null + session_id: string | null + payload_json: string | null + result: string + bytes_in: number + bytes_out: number +} + +const insertAuditStmt = db.prepare(` +INSERT INTO audit_log + (ts, action, source_device, target_device, session_id, payload_json, result, bytes_in, bytes_out) +VALUES + (@ts, @action, @source_device, @target_device, @session_id, @payload_json, @result, @bytes_in, @bytes_out) +`) + +export function recordAudit(args: { + action: string + source?: string | null + target?: string | null + sessionId?: string | null + payload?: any + result: 'ok' | 'denied' | 'error' | 'closed' + bytesIn?: number + bytesOut?: number +}): void { + insertAuditStmt.run({ + ts: Date.now(), + action: args.action, + source_device: args.source ?? null, + target_device: args.target ?? null, + session_id: args.sessionId ?? null, + payload_json: args.payload !== undefined ? JSON.stringify(args.payload) : null, + result: args.result, + bytes_in: args.bytesIn ?? 0, + bytes_out: args.bytesOut ?? 0, + }) +} + +export function listAudit(opts: { limit?: number; since?: number; sessionId?: string } = {}): AuditRow[] { + const limit = Math.min(Math.max(opts.limit ?? 200, 1), 2000) + if (opts.sessionId) { + return db.prepare(`SELECT * FROM audit_log WHERE session_id = ? ORDER BY ts DESC LIMIT ?`).all(opts.sessionId, limit) as AuditRow[] + } + if (opts.since) { + return db.prepare(`SELECT * FROM audit_log WHERE ts >= ? ORDER BY ts DESC LIMIT ?`).all(opts.since, limit) as AuditRow[] + } + return db.prepare(`SELECT * FROM audit_log ORDER BY ts DESC LIMIT ?`).all(limit) as AuditRow[] +} + +export function pruneAudit(olderThanMs: number): number { + const r = db.prepare(`DELETE FROM audit_log WHERE ts < ?`).run(Date.now() - olderThanMs) + return r.changes +} + +// 远程操作白名单 (按 deviceId -> { terminal, forward, usb }) +// 写入 settings.json (electron-store) 而不是 DB, 便于跨设备查看与回滚 +export interface RemoteAllowRow { + device_id: string + terminal: number // 0/1 + forward: number + usb: number +} diff --git a/src/main/index.ts b/src/main/index.ts index ab999dd..3b5cb5a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,8 +14,11 @@ import { Discovery } from './discovery' import { ChatServer } from './chat-server' import { ChatClient } from './chat-client' import { FileServer } from './file-server' +import { TerminalManager } from './remote/terminal' +import { ForwardManager } from './remote/forward' +import { UsbBridgeManager } from './remote/usb' import { registerIpc, bindNetworkContext } from './ipc' -import { listDevices, totalUnread, getUnread } from './db' +import { listDevices, totalUnread, getUnread, pruneAudit } from './db' import { setBadgeCount } from './notify' import { DEFAULT_PORTS, PROTOCOL_VERSION } from './protocol' import type { DeviceInfo } from './protocol' @@ -58,6 +61,9 @@ async function main() { const chatServer = new ChatServer(DEFAULT_PORTS.chat) const chatClient = new ChatClient(self) const fileServer = new FileServer(DEFAULT_PORTS.file, ensureDownloadDir()) + const terminal = new TerminalManager(chatClient, self.deviceId) + const forward = new ForwardManager(chatClient) + const usb = new UsbBridgeManager(chatClient) const chatPort = await chatServer.start() const filePort = await fileServer.start() @@ -65,10 +71,18 @@ async function main() { self.filePort = filePort // 后台清理 24h+ 残留 .tmp (不阻塞启动) fileServer.cleanupStaleTmp().catch(() => {}) - stopFns.push(() => chatServer.stop(), () => chatClient.stop(), () => discovery.stop(), () => fileServer.stop()) + stopFns.push( + () => chatServer.stop(), + () => chatClient.stop(), + () => discovery.stop(), + () => fileServer.stop(), + () => terminal.stop(), + () => forward.stop(), + () => usb.stop(), + ) await discovery.start() - bindNetworkContext({ self, discovery, chatServer, chatClient, fileServer }) + bindNetworkContext({ self, discovery, chatServer, chatClient, fileServer, terminal, forward, usb }) // 已入库的设备: 启动后主动尝试连接 for (const d of listDevices()) { @@ -91,6 +105,11 @@ async function main() { createTray() registerIpc() + // 启动时按 auditRetentionDays 清理一次旧审计 + const s = getSettings() + const retentionMs = Math.max(1, s.auditRetentionDays) * 24 * 3600 * 1000 + pruneAudit(retentionMs) + setTimeout(pushInitialState, 400) } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 009a950..7ddac2e 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -10,7 +10,8 @@ import { db, upsertDevice, listDevices, listMessages, insertMessage, getMessage, updateMessageStatus, updateMessageBody, deleteMessage, conversationKey, incrUnread, clearUnread, getUnread, totalUnread, listPendingMessages, listAllPending, listSentFileUpdates, - setDeviceIgnored, listIgnoredDevices, getDevice, deleteConversation + setDeviceIgnored, listIgnoredDevices, getDevice, deleteConversation, + recordAudit, listAudit, pruneAudit } from './db' import { Discovery, listNetInterfaces, type NetInterface } from './discovery' import { ChatServer } from './chat-server' @@ -19,7 +20,11 @@ import { FileServer, uploadFile, uploadBuffer } from './file-server' import { broadcastToRenderer, focusMainWindow } from './window' import { rebuildMenu } from './tray' import { notify, setBadgeCount } from './notify' -import type { DeviceInfo, MessageEnvelope, MessageBody, ImageMessage, FileMessage, TextMessage, SystemMessage, ReplyRef } from './protocol' +import { TerminalManager } from './remote/terminal' +import { ForwardManager } from './remote/forward' +import { UsbBridgeManager } from './remote/usb' +import { enqueueApproval, listPendingApprovals, replyApproval } from './remote/approval' +import type { DeviceInfo, MessageEnvelope, MessageBody, ImageMessage, FileMessage, TextMessage, SystemMessage, ReplyRef, TerminalFrame, ForwardFrame, UsbFrame, UsbDeviceInfo, UsbDirection, UsbAttachConfig } from './protocol' interface NetCtx { self: DeviceInfo @@ -27,6 +32,9 @@ interface NetCtx { chatServer: ChatServer chatClient: ChatClient fileServer: FileServer + terminal: TerminalManager + forward: ForwardManager + usb: UsbBridgeManager } let ctx: NetCtx | null = null @@ -159,6 +167,40 @@ export function bindNetworkContext(c: NetCtx) { else nextStatus = 'delivered' updateMessageStatus(messageId, nextStatus) broadcastToRenderer('message:statusChanged', { messageId, status: nextStatus }) + return + } + if (frame.type === 'terminal') { + ctx!.terminal.handleIncoming(peer, frame.payload as TerminalFrame) + .catch((e) => console.warn('[terminal] handle error:', e?.message)) + return + } + if (frame.type === 'forward') { + ctx!.forward.handleIncoming(peer, frame.payload as ForwardFrame) + return + } + if (frame.type === 'usb') { + ctx!.usb.handleIncoming(peer, frame.payload as UsbFrame) + return + } + }) + + // chat-server 收到的所有 ws 帧 (来自其他设备的入站) + // 这里分发 remote 帧给本地 manager (peer -> 我们) — 控制端发的请求落在这里 + c.chatServer.on('frame', (peer, frame) => { + if (frame.type === 'terminal') { + console.log(`[ipc] incoming terminal frame from ${peer.name}: ${frame.payload.type}`) + ctx!.terminal.handleIncoming(peer, frame.payload as TerminalFrame) + .catch((e) => console.warn('[terminal] handle error:', e?.message)) + return + } + if (frame.type === 'forward') { + console.log(`[ipc] incoming forward frame from ${peer.name}: ${(frame.payload as any).type}`) + ctx!.forward.handleIncoming(peer, frame.payload as ForwardFrame) + return + } + if (frame.type === 'usb') { + ctx!.usb.handleIncoming(peer, frame.payload as UsbFrame) + return } }) @@ -234,6 +276,57 @@ export function bindNetworkContext(c: NetCtx) { broadcastToRenderer('message:statusChanged', { messageId: r.message_id, status: 'read' }) } }) + + // ===== Remote: terminal / forward / usb 事件桥接 ===== + c.terminal.on('output', (sessionId: string, bytes: Buffer, peerId: string) => { + // 控制端从被控端收到 output -> 给 UI + broadcastToRenderer('remote:terminal:output', { sessionId, peerId, data: bytes.toString('base64') }) + }) + c.terminal.on('sessionOpened', (sessionId: string, peerId: string, info: { shell: string; rows: number; cols: number }) => { + broadcastToRenderer('remote:terminal:opened', { sessionId, peerId, ...info }) + }) + c.terminal.on('sessionClosed', (sessionId: string, peerId: string, reason?: string) => { + broadcastToRenderer('remote:terminal:closed', { sessionId, peerId, reason }) + }) + c.terminal.on('approvalRequested', (req) => { + broadcastToRenderer('remote:approval:requested', { ...req, kind: 'terminal' }) + }) + + c.forward.on('sessionOpened', (info) => { + broadcastToRenderer('remote:forward:opened', info) + }) + c.forward.on('sessionClosed', (info) => { + broadcastToRenderer('remote:forward:closed', info) + }) + c.forward.on('sessionError', (info) => { + broadcastToRenderer('remote:forward:error', info) + }) + c.forward.on('approvalRequested', (req) => { + broadcastToRenderer('remote:approval:requested', { ...req, kind: 'forward' }) + }) + + c.usb.on('output', (sessionId: string, bytes: Buffer, peerId: string) => { + broadcastToRenderer('remote:usb:output', { sessionId, peerId, data: bytes.toString('base64') }) + }) + c.usb.on('sessionOpened', (info) => { + broadcastToRenderer('remote:usb:opened', info) + }) + c.usb.on('sessionClosed', (info) => { + broadcastToRenderer('remote:usb:closed', info) + }) + c.usb.on('sessionError', (info) => { + broadcastToRenderer('remote:usb:error', info) + }) + c.usb.on('approvalRequested', (req) => { + broadcastToRenderer('remote:approval:requested', { ...req, kind: 'usb' }) + }) + + // 对端 ws 断开 -> 清理该对端的所有 session + c.chatClient.on('close', (peer) => { + c.terminal.onPeerDisconnected(peer.deviceId) + c.forward.onPeerDisconnected(peer.deviceId) + c.usb.onPeerDisconnected(peer.deviceId) + }) } function notifyIfNeeded(from: DeviceInfo, env: MessageEnvelope) { @@ -911,6 +1004,147 @@ export function registerIpc() { return false } }) + + // ===== 远程: terminal ===== + + ipcMain.handle('terminal:open', async (_e, args: { peerId: string; rows?: number; cols?: number; readOnly?: boolean }) => { + if (!ctx) return { ok: false, reason: 'no ctx' } + return ctx.terminal.openOnPeer(args.peerId, { + rows: args.rows ?? 24, + cols: args.cols ?? 80, + readOnly: args.readOnly, + }) + }) + + ipcMain.handle('terminal:input', (_e, args: { sessionId: string; dataBase64: string }) => { + if (!ctx) return false + if (!args?.sessionId || !args?.dataBase64) return false + const buf = Buffer.from(args.dataBase64, 'base64') + return ctx.terminal.sendInput(args.sessionId, buf) + }) + + ipcMain.handle('terminal:resize', (_e, args: { sessionId: string; rows: number; cols: number }) => { + if (!ctx) return false + return ctx.terminal.resize(args.sessionId, args.rows, args.cols) + }) + + ipcMain.handle('terminal:close', (_e, args: { sessionId: string; reason?: string }) => { + if (!ctx) return false + ctx.terminal.close(args.sessionId, args.reason) + return true + }) + + ipcMain.handle('terminal:listSessions', () => { + if (!ctx) return { client: [], server: [] } + return { + client: ctx.terminal.listClientSessions(), + server: ctx.terminal.listServerSessions(), + } + }) + + // ===== 远程: forward ===== + + ipcMain.handle('forward:open', async (_e, args: { peerId: string; direction?: 'self-out' | 'self-in'; listenPort: number; targetHost: string; targetPort: number; ttlSec?: number }) => { + if (!ctx) return { ok: false, reason: 'no ctx' } + return ctx.forward.openOnPeer(args.peerId, args) + }) + + ipcMain.handle('forward:close', (_e, args: { sessionId: string }) => { + if (!ctx) return false + // 控制端: 关 clientSession + ctx.forward.cleanupClientSession(args.sessionId, 'user') + return true + }) + + ipcMain.handle('forward:listSessions', () => { + if (!ctx) return { client: [], server: [] } + return { + client: ctx.forward.listClientSessions(), + server: ctx.forward.listServerSessions(), + } + }) + + // ===== 远程: usb (3 种模式: serial 字节流 / usb libusb 字节桥 / usbip 真透传) ===== + + ipcMain.handle('usb:list', async (_e, args: { peerId: string }) => { + if (!ctx) return { ok: false, reason: 'no ctx' } + return ctx.usb.listOnPeer(args.peerId) + }) + + // 列本机 (串口 + libusb + linux usbip) — self-in 模式用 + ipcMain.handle('usb:listLocal', async () => { + if (!ctx) return { ok: false, reason: 'no ctx' } + try { + const devices = await ctx.usb.listLocal() + return { ok: true, devices } + } catch (e: any) { + return { ok: false, reason: String(e?.message || e), devices: [] } + } + }) + + // attach: 由 renderer 把 UsbAttachConfig 传过来 + ipcMain.handle('usb:attach', async (_e, args: { peerId: string; direction?: UsbDirection; busId: string; config: UsbAttachConfig }) => { + if (!ctx) return { ok: false, reason: 'no ctx' } + return ctx.usb.attach(args.peerId, { direction: args.direction, busId: args.busId, config: args.config }) + }) + + // 串口: 发送字节 (client 端) + ipcMain.handle('usb:serialSend', (_e, args: { sessionId: string; dataBase64: string }) => { + if (!ctx) return false + const buf = Buffer.from(args.dataBase64, 'base64') + return ctx.usb.sendSerialBytes(args.sessionId, buf) + }) + + // USB 控制传输 (control OUT / control IN) + ipcMain.handle('usb:ctrlOut', async (_e, args: { sessionId: string; setup: { requestType: number; request: number; value: number; index: number }; dataBase64?: string }) => { + if (!ctx) return { ok: false, status: -1 } + return ctx.usb.usbCtrlTransfer(args.sessionId, args.setup, args.dataBase64) + }) + ipcMain.handle('usb:ctrlIn', async (_e, args: { sessionId: string; setup: { requestType: number; request: number; value: number; index: number }; length: number }) => { + if (!ctx) return { ok: false, status: -1 } + return ctx.usb.usbCtrlIn(args.sessionId, args.setup, args.length) + }) + + // USB 批量传输 (bulk OUT / bulk IN) + ipcMain.handle('usb:bulkOut', async (_e, args: { sessionId: string; endpoint: number; dataBase64: string }) => { + if (!ctx) return { ok: false, status: -1 } + return ctx.usb.usbBulkOut(args.sessionId, args.endpoint, args.dataBase64) + }) + ipcMain.handle('usb:bulkIn', async (_e, args: { sessionId: string; endpoint: number; length: number; timeoutMs?: number }) => { + if (!ctx) return { ok: false, status: -1 } + return ctx.usb.usbBulkIn(args.sessionId, args.endpoint, args.length, args.timeoutMs) + }) + + ipcMain.handle('usb:detach', async (_e, args: { sessionId: string }) => { + if (!ctx) return { ok: false, reason: 'no ctx' } + return ctx.usb.detach(args.sessionId, 'user') + }) + + ipcMain.handle('usb:listSessions', () => { + if (!ctx) return { client: [], server: [] } + return { + client: ctx.usb.listClientSessions(), + server: ctx.usb.listServerSessions(), + } + }) + + // ===== 授权 / 审计 ===== + + ipcMain.handle('remote:approval:list', () => listPendingApprovals()) + + ipcMain.handle('remote:approval:reply', (_e, args: { requestId: string; ok: boolean; remember?: boolean }) => { + return replyApproval(args.requestId, args.ok, args.remember) + }) + + ipcMain.handle('audit:list', (_e, args: { limit?: number; since?: number; sessionId?: string } = {}) => { + return listAudit(args || {}) + }) + + ipcMain.handle('audit:prune', (_e, olderThanMs?: number) => { + const days = getSettings().auditRetentionDays + const ms = olderThanMs ?? Math.max(1, days) * 24 * 3600 * 1000 + return pruneAudit(ms) + }) } function rowToEnvelope(row: any): MessageEnvelope { diff --git a/src/main/protocol.ts b/src/main/protocol.ts index 3ebd9fb..645816a 100644 --- a/src/main/protocol.ts +++ b/src/main/protocol.ts @@ -36,6 +36,135 @@ export type WsFrame = | { type: 'recall'; messageId: string; ts: number } | { type: 'typing'; from: string; ts: number } | { type: 'read'; upTo: string; ts: number } // 接收方发, upTo = 看到的最后一条 messageId; ≤ upTo 的发送方消息视为已读 + | { type: 'terminal'; payload: TerminalFrame } // 远程终端 (双向多路复用) + | { type: 'forward'; payload: ForwardFrame } // 端口转发 + | { type: 'usb'; payload: UsbFrame } // USB 设备透传 + +// ============ 远程终端 ============ + +export type TerminalFrame = + | { type: 'open'; sessionId: string; rows: number; cols: number; shell?: string; readOnly?: boolean } + | { type: 'input'; sessionId: string; data: string /* base64 of bytes */ } + | { type: 'output'; sessionId: string; data: string /* base64 of bytes */ } + | { type: 'resize'; sessionId: string; rows: number; cols: number } + | { type: 'close'; sessionId: string; reason?: string } + | { type: 'ack'; sessionId: string; ok: boolean; reason?: string } + +// ============ 端口转发 ============ + +/** + * 转发方向: + * 'self-out': 控制端作为代理入口 — 控制端开 127.0.0.1 TCP server, 被控端连自己的目标 + * 用途: 把对方的 127.0.0.1:8080 "挪到" 自己的 localhost 访问 + * 'self-in': 控制端作为被代理目标 — 控制端连自己的目标, 被控端开 127.0.0.1 TCP server + * 用途: 把自己的 127.0.0.1:8080 暴露到对方的 localhost 让对方访问 + */ +export type ForwardDirection = 'self-out' | 'self-in' + +export type ForwardFrame = + | { type: 'open'; sessionId: string; direction: ForwardDirection; listenPort: number; targetHost: string; targetPort: number; ttlSec: number } + | { type: 'data'; sessionId: string; dir: 'c2s' | 's2c'; data: string /* base64 */; fin?: boolean } + | { type: 'close'; sessionId: string; reason?: string; bytesIn?: number; bytesOut?: number } + | { type: 'ack'; sessionId: string; ok: boolean; reason?: string } + +// ============ USB / 串口 共享 ============ +// +// 老实说: 纯 Node.js 在 Windows 上做不到 "USB 设备完全透明地变成本机 USB" — 那是商业软件 +// (VirtualHere / USB Network Gate / FlexiHub) 或 Linux usbip + WSL 才有的能力. +// +// 我们提供三种能力, 按场景取用: +// 1. 'serial' — 真正的双向字节流转发, 适合 USB-串口适配器 (CH340/CP210x/FTDI) 和原生串口. +// 在 OS 看来就是普通的串口, 桥接完整 (无协议). +// 2. 'usb' — libusb 字节桥: server 端 node-usb open + claim interface, client 端发 +// controlTransfer / bulkTransfer 请求并收结果. 适合访问自定义 USB 设备 +// (单片机, 编程器, 调试器). 不是透明 USB — 是远程调设备. +// 3. 'usbip' — Linux only, 调系统 usbip CLI 做真透明 USB 透传. 用户机器必须装了 +// usbip + 加载 vhci_hcd 模块. macOS/Windows 默认不可用, UI 会隐藏. + +export interface UsbDeviceInfo { + busId: string // 总线 id, 用于唯一识别 + attach + vid: number + pid: number + deviceClass: number + deviceSubclass: number + product?: string + manufacturer?: string + serialNumber?: string + port?: string // node-hid 的 path (HID 模式) + /** 'serial' = 字节流转发, 'usb' = libusb 字节桥 (control/bulk), 'usbip' = Linux 透明透传 */ + kind: 'serial' | 'usb' | 'usbip' + serialPath?: string // 串口路径, e.g. COM3 / /dev/ttyUSB0 + baudRate?: number // 串口默认波特率 +} + +/** + * USB 方向: 同 Forward + * 'self-out': 控制端使用对端的设备 (对端打开真实设备) + * 'self-in': 对端使用控制端的设备 (控制端打开真实设备) + */ +export type UsbDirection = 'self-out' | 'self-in' + +// attach 时附带的配置 (按 kind 区分) +export type UsbAttachConfig = + | { kind: 'serial'; baudRate?: number; dataBits?: 5|6|7|8; stopBits?: 1|2; parity?: 'none'|'even'|'odd'|'mark'|'space'; createVirtual?: boolean; virtualName?: string } + | { kind: 'usb'; configurationValue?: number; interfaceNumber?: number; detachKernelDriver?: boolean } + | { kind: 'usbip' } + +// attached 帧带回的设备元信息 (主要用于 usb 模式, 让 UI 知道有哪些 endpoint) +export interface UsbEndpointInfo { + endpointNumber: number + direction: 'in' | 'out' + transferType: 'control' | 'bulk' | 'interrupt' | 'isochronous' + packetSize: number +} + +export interface UsbAttachedInfo { + kind: 'serial' | 'usb' | 'usbip' + // usb 模式: 设备的 endpoint 列表 + endpoints?: UsbEndpointInfo[] + // 串口模式: 远端真实串口路径 + serialPath?: string + // 串口模式: 本机虚拟串口路径 (createVirtual=true 时填) + userVirtualPath?: string + // 设备基础信息 + vid?: number + pid?: number + product?: string + manufacturer?: string + serialNumber?: string +} + +// USB 控制传输 setup 包 (libusb 风格) +export interface UsbControlSetup { + requestType: number // bmRequestType: 方向 | 类型 | 接收方 + request: number // bRequest + value: number // wValue + index: number // wIndex +} + +export type UsbFrame = + | { type: 'list'; reqId: string } + | { type: 'devices'; reqId: string; devices: UsbDeviceInfo[] } + | { type: 'attach'; sessionId: string; direction: UsbDirection; busId: string; config: UsbAttachConfig } + | { type: 'attached'; sessionId: string; ok: boolean; reason?: string; info?: UsbAttachedInfo } + | { type: 'detach'; sessionId: string; reason?: string } + | { type: 'detached'; sessionId: string; ok: boolean; reason?: string } + + // 字节流 (serial 模式: 双向, 半关时 fin=true; usb 模式: 不用, 用 control/bulk) + | { type: 'data'; sessionId: string; dir: 'host->dev' | 'dev->host'; data: string /* base64 */; fin?: boolean } + + // USB 控制传输 (request-response, 用 reqId 配对) + | { type: 'ctrlOut'; sessionId: string; reqId: string; setup: UsbControlSetup; data?: string } + | { type: 'ctrlIn'; sessionId: string; reqId: string; setup: UsbControlSetup; length: number } + | { type: 'ctrlResult';sessionId: string; reqId: string; ok: boolean; data?: string; status?: number } + + // USB 批量/中断传输 (request-response, 用 reqId 配对) + | { type: 'bulkOut'; sessionId: string; reqId: string; endpoint: number; data: string } + | { type: 'bulkIn'; sessionId: string; reqId: string; endpoint: number; length: number; timeoutMs?: number } + | { type: 'bulkResult';sessionId: string; reqId: string; ok: boolean; data?: string; status?: number } + + | { type: 'ack'; reqId?: string; sessionId?: string; ok: boolean; reason?: string } + | { type: 'error'; sessionId: string; reason: string } // ============ 消息 ============ diff --git a/src/main/settings.ts b/src/main/settings.ts index 8543d79..75f634d 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -14,6 +14,18 @@ export interface Settings { preferredInterface: string /** 备选: 用户也可指定精确地址 (避免接口名漂移) */ preferredAddress: string + /** 远程功能总开关 (false = 完全禁用 terminal/forward/usb, 即使 allowPeers 里有白名单) */ + remoteEnabled: boolean + /** 每个对端的远程操作授权, key = deviceId, value = { terminal, forward, usb } */ + remoteAllowPeers: Record + /** 端口转发默认 TTL (秒), 上限 24h */ + forwardDefaultTtlSec: number + /** 端口转发默认字节速率上限 (bytes/s), 0 = 不限 */ + forwardMaxBytesPerSec: number + /** 远程终端 read-only 默认 (禁用 input) */ + terminalReadOnlyByDefault: boolean + /** 审计日志保留天数 */ + auditRetentionDays: number } export const store = new Store({ @@ -28,6 +40,12 @@ export const store = new Store({ theme: 'light', preferredInterface: '', preferredAddress: '', + remoteEnabled: true, + remoteAllowPeers: {}, + forwardDefaultTtlSec: 3600, + forwardMaxBytesPerSec: 10 * 1024 * 1024, + terminalReadOnlyByDefault: false, + auditRetentionDays: 30, }, }) diff --git a/src/preload/index.ts b/src/preload/index.ts index c66c8ef..35a77b0 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -65,4 +65,50 @@ contextBridge.exposeInMainWorld('api', { // 离线消息: 启动后让主进程把 pending 重新尝试一遍 flushPending: () => ipcRenderer.invoke('message:flushPending'), + + // 远程: 终端 + terminalOpen: (peerId: string, opts?: { rows?: number; cols?: number; readOnly?: boolean }) => + ipcRenderer.invoke('terminal:open', { peerId, ...(opts || {}) }), + terminalInput: (sessionId: string, dataBase64: string) => + ipcRenderer.invoke('terminal:input', { sessionId, dataBase64 }), + terminalResize: (sessionId: string, rows: number, cols: number) => + ipcRenderer.invoke('terminal:resize', { sessionId, rows, cols }), + terminalClose: (sessionId: string, reason?: string) => + ipcRenderer.invoke('terminal:close', { sessionId, reason }), + terminalListSessions: () => ipcRenderer.invoke('terminal:listSessions'), + + // 远程: 端口转发 + forwardOpen: (peerId: string, args: { direction?: 'self-out' | 'self-in'; listenPort: number; targetHost: string; targetPort: number; ttlSec?: number }) => + ipcRenderer.invoke('forward:open', { peerId, ...args }), + forwardClose: (sessionId: string) => ipcRenderer.invoke('forward:close', { sessionId }), + forwardListSessions: () => ipcRenderer.invoke('forward:listSessions'), + + // 远程: USB / 串口 (3 种模式: serial 字节流 / usb libusb 字节桥 / usbip 真透传) + usbList: (peerId: string) => ipcRenderer.invoke('usb:list', { peerId }), + usbListLocal: () => ipcRenderer.invoke('usb:listLocal'), + usbAttach: (peerId: string, busId: string, opts: { direction?: 'self-out' | 'self-in'; config: any }) => + ipcRenderer.invoke('usb:attach', { peerId, busId, ...(opts || {}) }), + // 串口: 发送字节 + usbSerialSend: (sessionId: string, dataBase64: string) => + ipcRenderer.invoke('usb:serialSend', { sessionId, dataBase64 }), + // USB: 控制传输 + usbCtrlOut: (sessionId: string, setup: any, dataBase64?: string) => + ipcRenderer.invoke('usb:ctrlOut', { sessionId, setup, dataBase64 }), + usbCtrlIn: (sessionId: string, setup: any, length: number) => + ipcRenderer.invoke('usb:ctrlIn', { sessionId, setup, length }), + // USB: 批量传输 + usbBulkOut: (sessionId: string, endpoint: number, dataBase64: string) => + ipcRenderer.invoke('usb:bulkOut', { sessionId, endpoint, dataBase64 }), + usbBulkIn: (sessionId: string, endpoint: number, length: number, timeoutMs?: number) => + ipcRenderer.invoke('usb:bulkIn', { sessionId, endpoint, length, timeoutMs }), + usbDetach: (sessionId: string) => ipcRenderer.invoke('usb:detach', { sessionId }), + usbListSessions: () => ipcRenderer.invoke('usb:listSessions'), + + // 授权 / 审计 + remoteApprovalList: () => ipcRenderer.invoke('remote:approval:list'), + remoteApprovalReply: (requestId: string, ok: boolean, remember?: boolean) => + ipcRenderer.invoke('remote:approval:reply', { requestId, ok, remember }), + auditList: (args?: { limit?: number; since?: number; sessionId?: string }) => + ipcRenderer.invoke('audit:list', args || {}), + auditPrune: (olderThanMs?: number) => ipcRenderer.invoke('audit:prune', olderThanMs), }) diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index fdd2de4..c84a980 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -1,18 +1,24 @@