diff --git a/app/electron.vite.config.mjs b/app/electron.vite.config.mjs index 6b30e11..6c9bd34 100644 --- a/app/electron.vite.config.mjs +++ b/app/electron.vite.config.mjs @@ -3,7 +3,13 @@ import { defineConfig } from 'electron-vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ - main: {}, + main: { + build: { + rollupOptions: { + external: ['mqtt'] + } + } + }, preload: {}, renderer: { resolve: { diff --git a/app/package.json b/app/package.json index a08a3e7..fd4e53e 100644 --- a/app/package.json +++ b/app/package.json @@ -19,7 +19,8 @@ }, "dependencies": { "@electron-toolkit/preload": "^3.0.2", - "@electron-toolkit/utils": "^4.0.0" + "@electron-toolkit/utils": "^4.0.0", + "mqtt": "^5.10.3" }, "devDependencies": { "@electron-toolkit/eslint-config": "^2.1.0", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index b6b84fe..2365ceb 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@electron-toolkit/utils': specifier: ^4.0.0 version: 4.0.0(electron@39.8.10) + mqtt: + specifier: ^5.10.3 + version: 5.15.2 devDependencies: '@electron-toolkit/eslint-config': specifier: ^2.1.0 @@ -122,6 +125,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -823,9 +830,15 @@ packages: '@types/node@26.1.1': resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + '@types/readable-stream@4.0.24': + resolution: {integrity: sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==} + '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -873,6 +886,10 @@ packages: resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} engines: {node: ^20.17.0 || >=22.9.0} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -950,6 +967,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bl@6.1.6: + resolution: {integrity: sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==} + bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} @@ -970,6 +990,9 @@ packages: resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} + broker-factory@3.1.15: + resolution: {integrity: sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==} + browserslist@4.28.6: resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -981,6 +1004,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + builder-util-runtime@9.7.0: resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==} engines: {node: '>=12.0.0'} @@ -1061,6 +1087,9 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} + commist@3.2.0: + resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==} + compare-version@0.1.2: resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} engines: {node: '>=0.10.0'} @@ -1068,6 +1097,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1343,6 +1376,14 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + evtd@0.2.4: resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==} @@ -1366,6 +1407,10 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-unique-numbers@9.0.27: + resolution: {integrity: sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==} + engines: {node: '>=18.2.0'} + fast-uri@3.1.3: resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} @@ -1512,6 +1557,9 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + highlight.js@11.11.1: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} @@ -1535,6 +1583,9 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1554,6 +1605,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1597,6 +1652,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + js-sdsl@4.3.0: + resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1662,6 +1720,9 @@ packages: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1731,6 +1792,14 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true + mqtt-packet@9.0.2: + resolution: {integrity: sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==} + + mqtt@5.15.2: + resolution: {integrity: sha512-VWZU2CSUY3U3oN0PSBRDE5SNsFi4zqqNeQ/uv3pZWqY3CrBXD/dhd0ZLjlsk5YnebGlrapi4lRVNJPUNQ5aZ3w==} + engines: {node: '>=16.0.0'} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1779,6 +1848,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + number-allocator@1.0.14: + resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -1873,6 +1945,10 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -1909,6 +1985,14 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1935,6 +2019,9 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -1952,6 +2039,9 @@ packages: safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + sanitize-filename@1.6.4: resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} @@ -2002,6 +2092,14 @@ packages: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2013,6 +2111,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} @@ -2027,6 +2129,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2089,6 +2194,9 @@ packages: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -2216,6 +2324,18 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + worker-factory@7.0.50: + resolution: {integrity: sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==} + + worker-timers-broker@8.0.18: + resolution: {integrity: sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==} + + worker-timers-worker@9.0.15: + resolution: {integrity: sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==} + + worker-timers@8.0.33: + resolution: {integrity: sha512-RQVlKkek80v8M6SHvdMKiywaXFmX3XTpYTXTw0r62PdXB06t74XNxcTuG8wsQwnjorAQymNQyyQ0rzv7PykEMQ==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2223,6 +2343,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -2346,6 +2478,8 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 + '@babel/runtime@7.29.7': {} + '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -2905,10 +3039,18 @@ snapshots: dependencies: undici-types: 8.3.0 + '@types/readable-stream@4.0.24': + dependencies: + '@types/node': 26.1.1 + '@types/responselike@1.0.3': dependencies: '@types/node': 22.20.1 + '@types/ws@8.18.1': + dependencies: + '@types/node': 26.1.1 + '@types/yauzl@2.10.3': dependencies: '@types/node': 22.20.1 @@ -2978,6 +3120,10 @@ snapshots: abbrev@4.0.0: {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: acorn: 8.17.0 @@ -3082,6 +3228,13 @@ snapshots: baseline-browser-mapping@2.10.43: {} + bl@6.1.6: + dependencies: + '@types/readable-stream': 4.0.24 + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 4.7.0 + bluebird@3.7.2: {} boolbase@1.0.0: {} @@ -3102,6 +3255,13 @@ snapshots: dependencies: balanced-match: 4.0.4 + broker-factory@3.1.15: + dependencies: + '@babel/runtime': 7.29.7 + fast-unique-numbers: 9.0.27 + tslib: 2.8.1 + worker-factory: 7.0.50 + browserslist@4.28.6: dependencies: baseline-browser-mapping: 2.10.43 @@ -3114,6 +3274,11 @@ snapshots: buffer-from@1.1.2: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + builder-util-runtime@9.7.0: dependencies: debug: 4.4.3 @@ -3203,10 +3368,19 @@ snapshots: commander@9.5.0: optional: true + commist@3.2.0: {} + compare-version@0.1.2: {} concat-map@0.0.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + convert-source-map@2.0.0: {} core-util-is@1.0.3: {} @@ -3577,6 +3751,10 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + + events@3.3.0: {} + evtd@0.2.4: {} exponential-backoff@3.1.3: {} @@ -3599,6 +3777,11 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-unique-numbers@9.0.27: + dependencies: + '@babel/runtime': 7.29.7 + tslib: 2.8.1 + fast-uri@3.1.3: {} fd-slicer@1.1.0: @@ -3775,6 +3958,8 @@ snapshots: dependencies: function-bind: 1.1.2 + help-me@5.0.0: {} + highlight.js@11.11.1: {} hosted-git-info@4.1.0: @@ -3802,6 +3987,8 @@ snapshots: transitivePeerDependencies: - supports-color + ieee754@1.2.1: {} + ignore@5.3.2: {} import-fresh@3.3.1: @@ -3818,6 +4005,8 @@ snapshots: inherits@2.0.4: {} + ip-address@10.2.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -3846,6 +4035,8 @@ snapshots: jiti@2.7.0: {} + js-sdsl@4.3.0: {} + js-tokens@4.0.0: {} js-yaml@4.3.0: @@ -3900,6 +4091,8 @@ snapshots: lowercase-keys@2.0.0: {} + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3959,6 +4152,37 @@ snapshots: dependencies: minimist: 1.2.8 + mqtt-packet@9.0.2: + dependencies: + bl: 6.1.6 + debug: 4.4.3 + process-nextick-args: 2.0.1 + transitivePeerDependencies: + - supports-color + + mqtt@5.15.2: + dependencies: + '@types/readable-stream': 4.0.24 + '@types/ws': 8.18.1 + commist: 3.2.0 + concat-stream: 2.0.0 + debug: 4.4.3 + help-me: 5.0.0 + lru-cache: 10.4.3 + minimist: 1.2.8 + mqtt-packet: 9.0.2 + number-allocator: 1.0.14 + readable-stream: 4.7.0 + rfdc: 1.4.1 + socks: 2.8.9 + split2: 4.2.0 + worker-timers: 8.0.33 + ws: 8.21.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + ms@2.1.3: {} naive-ui@2.44.1(vue@3.5.39): @@ -4022,6 +4246,13 @@ snapshots: dependencies: boolbase: 1.0.0 + number-allocator@1.0.14: + dependencies: + debug: 4.4.3 + js-sdsl: 4.3.0 + transitivePeerDependencies: + - supports-color + object-keys@1.1.1: optional: true @@ -4109,6 +4340,8 @@ snapshots: process-nextick-args@2.0.1: {} + process@0.11.10: {} + progress@2.0.3: {} promise-retry@2.0.1: @@ -4153,6 +4386,20 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -4171,6 +4418,8 @@ snapshots: retry@0.12.0: {} + rfdc@1.4.1: {} + rimraf@2.6.3: dependencies: glob: 7.2.3 @@ -4218,6 +4467,8 @@ snapshots: safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + sanitize-filename@1.6.4: dependencies: truncate-utf8-bytes: 1.0.2 @@ -4254,6 +4505,13 @@ snapshots: dependencies: semver: 7.8.5 + smart-buffer@4.2.0: {} + + socks@2.8.9: + dependencies: + ip-address: 10.2.0 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -4263,6 +4521,8 @@ snapshots: source-map@0.6.1: {} + split2@4.2.0: {} + sprintf-js@1.1.3: optional: true @@ -4278,6 +4538,10 @@ snapshots: dependencies: safe-buffer: 5.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4346,6 +4610,8 @@ snapshots: type-fest@0.13.1: optional: true + typedarray@0.0.6: {} + undici-types@6.21.0: {} undici-types@8.3.0: {} @@ -4454,6 +4720,33 @@ snapshots: word-wrap@1.2.5: {} + worker-factory@7.0.50: + dependencies: + '@babel/runtime': 7.29.7 + fast-unique-numbers: 9.0.27 + tslib: 2.8.1 + + worker-timers-broker@8.0.18: + dependencies: + '@babel/runtime': 7.29.7 + broker-factory: 3.1.15 + fast-unique-numbers: 9.0.27 + tslib: 2.8.1 + worker-timers-worker: 9.0.15 + + worker-timers-worker@9.0.15: + dependencies: + '@babel/runtime': 7.29.7 + tslib: 2.8.1 + worker-factory: 7.0.50 + + worker-timers@8.0.33: + dependencies: + '@babel/runtime': 7.29.7 + tslib: 2.8.1 + worker-timers-broker: 8.0.18 + worker-timers-worker: 9.0.15 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -4462,6 +4755,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.21.1: {} + xml-name-validator@4.0.0: {} xmlbuilder@15.1.1: {} diff --git a/app/src/main/index.js b/app/src/main/index.js index 61e8bf2..2960d4a 100644 --- a/app/src/main/index.js +++ b/app/src/main/index.js @@ -1,13 +1,16 @@ -import { app, shell, BrowserWindow, ipcMain } from 'electron' +import { app, shell, BrowserWindow } from 'electron' import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' +import { registerIpcHandlers } from './ipc-handlers.js' function createWindow() { // Create the browser window. const mainWindow = new BrowserWindow({ - width: 900, - height: 670, + width: 1200, + height: 800, + minWidth: 900, + minHeight: 600, show: false, autoHideMenuBar: true, ...(process.platform === 'linux' ? { icon } : {}), @@ -49,8 +52,8 @@ app.whenReady().then(() => { optimizer.watchWindowShortcuts(window) }) - // IPC test - ipcMain.on('ping', () => console.log('pong')) + // 注册 IPC 处理器 + registerIpcHandlers() createWindow() diff --git a/app/src/main/ipc-handlers.js b/app/src/main/ipc-handlers.js new file mode 100644 index 0000000..58cfc32 --- /dev/null +++ b/app/src/main/ipc-handlers.js @@ -0,0 +1,119 @@ +import { ipcMain, BrowserWindow } from 'electron' +import mqttManager from './mqtt-manager.js' + +export function registerIpcHandlers() { + // 获取主窗口(用于推送事件) + function getMainWindow() { + const windows = BrowserWindow.getAllWindows() + return windows.length > 0 ? windows[0] : null + } + + // 向渲染进程发送事件 + function sendToRenderer(channel, data) { + const win = getMainWindow() + if (win && !win.isDestroyed()) { + win.webContents.send(channel, data) + } + } + + // 注册 MQTT 管理器的事件监听 + mqttManager.addListener((event, data) => { + switch (event) { + case 'status-change': + sendToRenderer('mqtt:status-change', data) + break + case 'message': + sendToRenderer('mqtt:message', data) + break + case 'subscription-change': + sendToRenderer('mqtt:subscription-change', data) + break + case 'subscription-error': + sendToRenderer('mqtt:subscription-error', data) + break + case 'publish-error': + sendToRenderer('mqtt:publish-error', data) + break + } + }) + + // ==================== 服务器管理 ==================== + ipcMain.handle('mqtt:get-servers', () => { + return mqttManager.getServers() + }) + + ipcMain.handle('mqtt:add-server', (_, config) => { + return mqttManager.addServer(config) + }) + + ipcMain.handle('mqtt:update-server', (_, id, config) => { + return mqttManager.updateServer(id, config) + }) + + ipcMain.handle('mqtt:delete-server', (_, id) => { + mqttManager.deleteServer(id) + return true + }) + + // ==================== 连接管理 ==================== + ipcMain.handle('mqtt:connect', (_, id) => { + return mqttManager.connect(id) + }) + + ipcMain.handle('mqtt:disconnect', (_, id) => { + mqttManager.disconnect(id) + return true + }) + + // ==================== 主题管理 ==================== + ipcMain.handle('mqtt:add-topic', (_, serverId, topic, qos) => { + return mqttManager.addTopic(serverId, topic, qos) + }) + + ipcMain.handle('mqtt:remove-topic', (_, serverId, topic) => { + return mqttManager.removeTopic(serverId, topic) + }) + + ipcMain.handle('mqtt:subscribe', (_, serverId, topic) => { + return mqttManager.subscribe(serverId, topic) + }) + + ipcMain.handle('mqtt:unsubscribe', (_, serverId, topic) => { + return mqttManager.unsubscribe(serverId, topic) + }) + + ipcMain.handle('mqtt:subscribe-all', (_, serverId) => { + return mqttManager.subscribeAll(serverId) + }) + + ipcMain.handle('mqtt:update-topic', (_, serverId, topicId, topic, qos) => { + return mqttManager.updateTopic(serverId, topicId, topic, qos) + }) + + // ==================== 消息发布 ==================== + ipcMain.handle('mqtt:publish', (_, serverId, topic, payload, opts) => { + return mqttManager.publish(serverId, topic, payload, opts) + }) + + // ==================== 消息管理 ==================== + ipcMain.handle('mqtt:get-messages', (_, serverId) => { + return mqttManager.getMessages(serverId) + }) + + ipcMain.handle('mqtt:get-publish-history', (_, serverId) => { + return mqttManager.getPublishHistory(serverId) + }) + + ipcMain.handle('mqtt:clear-messages', (_, serverId) => { + mqttManager.clearMessages(serverId) + return true + }) + + ipcMain.handle('mqtt:clear-publish-history', (_, serverId) => { + mqttManager.clearPublishHistory(serverId) + return true + }) + + // 兼容旧的 ping 测试 + ipcMain.on('ping', () => console.log('pong')) +} diff --git a/app/src/main/mqtt-manager.js b/app/src/main/mqtt-manager.js new file mode 100644 index 0000000..7c62636 --- /dev/null +++ b/app/src/main/mqtt-manager.js @@ -0,0 +1,524 @@ +import mqtt from 'mqtt' +import { app } from 'electron' +import { join } from 'path' +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs' + +class MqttManager { + constructor() { + this.clients = new Map() // serverId -> mqtt client + this.servers = new Map() // serverId -> server config + this.messages = new Map() // serverId -> message[] + this.publishHistory = new Map() // serverId -> publishHistory[] + this.listeners = new Set() // IPC callback functions + + this.dataPath = join(app.getPath('userData'), 'frost-mqtt-data.json') + this.loadData() + } + + // ==================== 数据持久化 ==================== + getDataDir() { + const dir = app.getPath('userData') + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }) + } + return dir + } + + loadData() { + try { + if (existsSync(this.dataPath)) { + const raw = readFileSync(this.dataPath, 'utf-8') + const data = JSON.parse(raw) + if (data.servers) { + data.servers.forEach((s) => { + s.status = 'disconnected' + s.topics = (s.topics || []).map((t) => ({ + ...t, + id: t.id || 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), + subscribed: false + })) + this.servers.set(s.id, s) + }) + } + if (data.messages) { + Object.entries(data.messages).forEach(([id, msgs]) => { + this.messages.set(id, msgs) + }) + } + if (data.publishHistory) { + Object.entries(data.publishHistory).forEach(([id, history]) => { + this.publishHistory.set(id, history) + }) + } + } else { + this.initDefaultServers() + } + } catch (e) { + console.error('加载数据失败:', e.message) + this.initDefaultServers() + } + } + + saveData() { + try { + const data = { + servers: Array.from(this.servers.values()).map((s) => ({ + ...s, + status: 'disconnected', + topics: s.topics.map((t) => ({ ...t, subscribed: false })) + })), + messages: Object.fromEntries(this.messages), + publishHistory: Object.fromEntries(this.publishHistory) + } + writeFileSync(this.dataPath, JSON.stringify(data, null, 2), 'utf-8') + } catch (e) { + console.error('保存数据失败:', e.message) + } + } + + initDefaultServers() { + const defaults = [ + { + id: 'srv_001', + name: 'EMQX 公共测试', + host: 'broker.emqx.io', + port: 1883, + protocol: 'mqtt', + clientId: 'frost_client_' + Math.random().toString(36).slice(2, 8), + username: '', + password: '', + keepAlive: 60, + cleanSession: true, + status: 'disconnected', + topics: [ + { id: 'topic_default_001', topic: 'sensor/+/temperature', qos: 1, subscribed: false }, + { id: 'topic_default_002', topic: 'device/status', qos: 2, subscribed: false } + ] + }, + { + id: 'srv_002', + name: '本地 Mosquitto', + host: 'localhost', + port: 1883, + protocol: 'mqtt', + clientId: 'frost_local_' + Math.random().toString(36).slice(2, 8), + username: '', + password: '', + keepAlive: 60, + cleanSession: true, + status: 'disconnected', + topics: [ + { id: 'topic_default_003', topic: 'home/+/light', qos: 1, subscribed: false }, + { id: 'topic_default_004', topic: 'home/alarm', qos: 2, subscribed: false } + ] + } + ] + defaults.forEach((s) => { + this.servers.set(s.id, s) + this.messages.set(s.id, []) + this.publishHistory.set(s.id, []) + }) + this.saveData() + } + + // ==================== 监听器 ==================== + addListener(callback) { + this.listeners.add(callback) + } + + removeListener(callback) { + this.listeners.delete(callback) + } + + notify(event, data) { + this.listeners.forEach((cb) => { + try { + cb(event, data) + } catch (e) { + console.error('通知失败:', e) + } + }) + } + + // ==================== 服务器管理 ==================== + getServers() { + return Array.from(this.servers.values()).map((s) => ({ + ...s, + password: s.password ? '******' : '' + })) + } + + getServer(id) { + return this.servers.get(id) || null + } + + addServer(config) { + const id = 'srv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6) + const server = { + id, + name: config.name, + host: config.host, + port: config.port || 1883, + protocol: config.protocol || 'mqtt', + clientId: config.clientId || 'frost_' + Math.random().toString(36).slice(2, 10), + username: config.username || '', + password: config.password || '', + keepAlive: config.keepAlive || 60, + cleanSession: config.cleanSession !== false, + status: 'disconnected', + topics: [] + } + this.servers.set(id, server) + this.messages.set(id, []) + this.publishHistory.set(id, []) + this.saveData() + return server + } + + updateServer(id, config) { + const server = this.servers.get(id) + if (!server) return null + Object.assign(server, { + name: config.name, + host: config.host, + port: config.port || 1883, + protocol: config.protocol || 'mqtt', + clientId: config.clientId || server.clientId, + username: config.username || '', + keepAlive: config.keepAlive || 60, + cleanSession: config.cleanSession !== false + }) + if (config.password) { + server.password = config.password + } + this.saveData() + return server + } + + deleteServer(id) { + // 先断开连接 + this.disconnect(id) + this.servers.delete(id) + this.messages.delete(id) + this.publishHistory.delete(id) + this.saveData() + } + + // ==================== 连接管理 ==================== + connect(id) { + const server = this.servers.get(id) + if (!server) return false + + // 如果已连接,先断开 + if (this.clients.has(id)) { + this.disconnect(id) + } + + server.status = 'connecting' + this.notify('status-change', { id, status: 'connecting' }) + + const protocol = server.protocol || 'mqtt' + const url = `${protocol}://${server.host}:${server.port}` + + const options = { + clientId: server.clientId, + username: server.username || undefined, + password: server.password || undefined, + keepalive: server.keepAlive, + clean: server.cleanSession, + reconnectPeriod: 5000, + connectTimeout: 10000 + } + + try { + const client = mqtt.connect(url, options) + + client.on('connect', () => { + server.status = 'connected' + this.notify('status-change', { id, status: 'connected' }) + // 自动订阅已有主题 + server.topics.forEach((t) => { + this._doSubscribe(id, t.topic, { qos: t.qos }) + }) + }) + + client.on('error', (err) => { + console.error(`[${server.name}] 连接错误:`, err.message) + server.status = 'error' + this.notify('status-change', { id, status: 'error', error: err.message }) + }) + + client.on('close', () => { + if (server.status === 'connected') { + server.status = 'disconnected' + server.topics.forEach((t) => { + t.subscribed = false + }) + this.notify('status-change', { id, status: 'disconnected' }) + } + }) + + client.on('message', (topic, payload, packet) => { + const msg = { + time: + new Date().toLocaleTimeString('zh-CN', { hour12: false }) + + '.' + + String(Date.now() % 1000).padStart(3, '0'), + topic, + payload: payload.toString(), + qos: packet.qos, + retain: packet.retain, + direction: 'sub', + id: 'msg_' + Date.now() + } + const msgs = this.messages.get(id) || [] + msgs.unshift(msg) + if (msgs.length > 500) msgs.pop() + this.messages.set(id, msgs) + this.notify('message', { serverId: id, message: msg }) + }) + + this.clients.set(id, client) + return true + } catch (err) { + server.status = 'error' + this.notify('status-change', { id, status: 'error', error: err.message }) + return false + } + } + + disconnect(id) { + const client = this.clients.get(id) + if (client) { + client.end(true) + this.clients.delete(id) + } + const server = this.servers.get(id) + if (server) { + server.status = 'disconnected' + server.topics.forEach((t) => { + t.subscribed = false + }) + this.notify('status-change', { id, status: 'disconnected' }) + } + } + + // ==================== 订阅管理 ==================== + _doSubscribe(serverId, topic, opts) { + const client = this.clients.get(serverId) + const server = this.servers.get(serverId) + if (!client || !server || server.status !== 'connected') return false + + client.subscribe(topic, opts, (err) => { + if (err) { + console.error(`订阅失败 [${topic}]:`, err.message) + this.notify('subscription-error', { serverId, topic, error: err.message }) + } else { + const t = server.topics.find((t) => t.topic === topic) + if (t) t.subscribed = true + this.notify('subscription-change', { serverId, topic, subscribed: true }) + } + }) + return true + } + + addTopic(serverId, topic, qos = 1) { + const server = this.servers.get(serverId) + if (!server) return false + if (server.topics.find((t) => t.topic === topic)) return false + + const topicId = 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6) + server.topics.push({ id: topicId, topic, qos, subscribed: false }) + this.saveData() + + // 如果已连接,自动订阅 + if (server.status === 'connected') { + this._doSubscribe(serverId, topic, { qos }) + } + return true + } + + removeTopic(serverId, topic) { + const server = this.servers.get(serverId) + if (!server) return false + + // 先取消订阅 + this.unsubscribe(serverId, topic) + server.topics = server.topics.filter((t) => t.topic !== topic) + this.saveData() + return true + } + + subscribe(serverId, topic) { + const server = this.servers.get(serverId) + if (!server) return false + const t = server.topics.find((t) => t.topic === topic) + if (!t) return false + return this._doSubscribe(serverId, topic, { qos: t.qos }) + } + + unsubscribe(serverId, topic) { + const client = this.clients.get(serverId) + const server = this.servers.get(serverId) + if (!client || !server || server.status !== 'connected') return false + + client.unsubscribe(topic, {}, (err) => { + if (!err) { + const t = server.topics.find((t) => t.topic === topic) + if (t) t.subscribed = false + this.notify('subscription-change', { serverId, topic, subscribed: false }) + } + }) + return true + } + + updateTopic(serverId, topicId, newTopic, newQos) { + const server = this.servers.get(serverId) + if (!server) return false + const t = server.topics.find((item) => item.id === topicId) + if (!t) return false + + const topicChanged = t.topic !== newTopic + if ( + topicChanged && + server.topics.find((item) => item.topic === newTopic && item.id !== topicId) + ) { + return false + } + + const oldTopic = t.topic + const wasSubscribed = t.subscribed + + // 如果主题名变更且已订阅,先取消旧主题订阅 + if (topicChanged && wasSubscribed) { + this.unsubscribe(serverId, oldTopic) + } + + t.topic = newTopic + t.qos = newQos + + // 如果已连接且此前已订阅,重新订阅新主题 + if (server.status === 'connected' && wasSubscribed) { + this._doSubscribe(serverId, newTopic, { qos: newQos }) + } + + this.saveData() + this.notify('subscription-change', { + serverId, + topic: newTopic, + subscribed: t.subscribed, + topicId + }) + return true + } + + subscribeAll(serverId) { + const client = this.clients.get(serverId) + const server = this.servers.get(serverId) + if (!client || !server || server.status !== 'connected') return false + + const topicsToSubscribe = server.topics.filter((t) => !t.subscribed) + if (topicsToSubscribe.length === 0) return true + + const subMap = Object.fromEntries(topicsToSubscribe.map((t) => [t.topic, { qos: t.qos }])) + + client.subscribe(subMap, (err) => { + if (err) { + console.error('批量订阅失败:', err.message) + topicsToSubscribe.forEach((t) => { + this.notify('subscription-error', { serverId, topic: t.topic, error: err.message }) + }) + } else { + topicsToSubscribe.forEach((t) => { + t.subscribed = true + this.notify('subscription-change', { + serverId, + topic: t.topic, + subscribed: true, + topicId: t.id + }) + }) + } + }) + return true + } + + // ==================== 消息发布 ==================== + publish(serverId, topic, payload, opts = {}) { + const client = this.clients.get(serverId) + const server = this.servers.get(serverId) + if (!client || !server || server.status !== 'connected') return false + + const pubOpts = { + qos: opts.qos ?? 1, + retain: opts.retain ?? false + } + + client.publish(topic, payload, pubOpts, (err) => { + if (err) { + console.error(`发布失败 [${topic}]:`, err.message) + this.notify('publish-error', { serverId, topic, error: err.message }) + } + }) + + // 记录发布历史 + const pubEntry = { + time: + new Date().toLocaleTimeString('zh-CN', { hour12: false }) + + '.' + + String(Date.now() % 1000).padStart(3, '0'), + topic, + payload, + qos: pubOpts.qos, + retain: pubOpts.retain, + direction: 'pub' + } + const history = this.publishHistory.get(serverId) || [] + history.unshift(pubEntry) + if (history.length > 50) history.pop() + this.publishHistory.set(serverId, history) + + // 同时添加到消息日志 + const msgs = this.messages.get(serverId) || [] + msgs.unshift({ + ...pubEntry, + id: 'msg_' + Date.now() + }) + if (msgs.length > 500) msgs.pop() + this.messages.set(serverId, msgs) + + this.notify('message', { + serverId, + message: { + ...pubEntry, + id: 'msg_' + Date.now() + } + }) + + this.saveData() + return true + } + + // ==================== 消息管理 ==================== + getMessages(serverId) { + return this.messages.get(serverId) || [] + } + + getPublishHistory(serverId) { + return this.publishHistory.get(serverId) || [] + } + + clearMessages(serverId) { + this.messages.set(serverId, []) + this.saveData() + } + + clearPublishHistory(serverId) { + this.publishHistory.set(serverId, []) + this.saveData() + } +} + +// 单例 +const mqttManager = new MqttManager() +export default mqttManager diff --git a/app/src/preload/index.js b/app/src/preload/index.js index 8d62cb9..0d414cb 100644 --- a/app/src/preload/index.js +++ b/app/src/preload/index.js @@ -1,8 +1,64 @@ -import { contextBridge } from 'electron' +import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload' // Custom APIs for renderer -const api = {} +const api = { + // 服务器管理 + getServers: () => ipcRenderer.invoke('mqtt:get-servers'), + addServer: (config) => ipcRenderer.invoke('mqtt:add-server', config), + updateServer: (id, config) => ipcRenderer.invoke('mqtt:update-server', id, config), + deleteServer: (id) => ipcRenderer.invoke('mqtt:delete-server', id), + + // 连接管理 + connect: (id) => ipcRenderer.invoke('mqtt:connect', id), + disconnect: (id) => ipcRenderer.invoke('mqtt:disconnect', id), + + // 主题管理 + addTopic: (serverId, topic, qos) => ipcRenderer.invoke('mqtt:add-topic', serverId, topic, qos), + removeTopic: (serverId, topic) => ipcRenderer.invoke('mqtt:remove-topic', serverId, topic), + subscribe: (serverId, topic) => ipcRenderer.invoke('mqtt:subscribe', serverId, topic), + unsubscribe: (serverId, topic) => ipcRenderer.invoke('mqtt:unsubscribe', serverId, topic), + subscribeAll: (serverId) => ipcRenderer.invoke('mqtt:subscribe-all', serverId), + updateTopic: (serverId, topicId, topic, qos) => + ipcRenderer.invoke('mqtt:update-topic', serverId, topicId, topic, qos), + + // 消息发布 + publish: (serverId, topic, payload, opts) => + ipcRenderer.invoke('mqtt:publish', serverId, topic, payload, opts), + + // 消息管理 + getMessages: (serverId) => ipcRenderer.invoke('mqtt:get-messages', serverId), + getPublishHistory: (serverId) => ipcRenderer.invoke('mqtt:get-publish-history', serverId), + clearMessages: (serverId) => ipcRenderer.invoke('mqtt:clear-messages', serverId), + clearPublishHistory: (serverId) => ipcRenderer.invoke('mqtt:clear-publish-history', serverId), + + // 事件监听 + onStatusChange: (callback) => { + const handler = (_, data) => callback(data) + ipcRenderer.on('mqtt:status-change', handler) + return () => ipcRenderer.removeListener('mqtt:status-change', handler) + }, + onMessage: (callback) => { + const handler = (_, data) => callback(data) + ipcRenderer.on('mqtt:message', handler) + return () => ipcRenderer.removeListener('mqtt:message', handler) + }, + onSubscriptionChange: (callback) => { + const handler = (_, data) => callback(data) + ipcRenderer.on('mqtt:subscription-change', handler) + return () => ipcRenderer.removeListener('mqtt:subscription-change', handler) + }, + onSubscriptionError: (callback) => { + const handler = (_, data) => callback(data) + ipcRenderer.on('mqtt:subscription-error', handler) + return () => ipcRenderer.removeListener('mqtt:subscription-error', handler) + }, + onPublishError: (callback) => { + const handler = (_, data) => callback(data) + ipcRenderer.on('mqtt:publish-error', handler) + return () => ipcRenderer.removeListener('mqtt:publish-error', handler) + } +} // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise diff --git a/app/src/renderer/index.html b/app/src/renderer/index.html index f05fc87..f5955ae 100644 --- a/app/src/renderer/index.html +++ b/app/src/renderer/index.html @@ -2,11 +2,10 @@ - Electron - + Frost MQTT Client @@ -14,4 +13,4 @@
- + \ No newline at end of file diff --git a/app/src/renderer/src/App.vue b/app/src/renderer/src/App.vue index 3988215..f24c282 100644 --- a/app/src/renderer/src/App.vue +++ b/app/src/renderer/src/App.vue @@ -1,24 +1,156 @@ + + diff --git a/app/src/renderer/src/components/ServerModal.vue b/app/src/renderer/src/components/ServerModal.vue new file mode 100644 index 0000000..71393f8 --- /dev/null +++ b/app/src/renderer/src/components/ServerModal.vue @@ -0,0 +1,166 @@ + + + diff --git a/app/src/renderer/src/components/Sidebar.vue b/app/src/renderer/src/components/Sidebar.vue new file mode 100644 index 0000000..ab1050e --- /dev/null +++ b/app/src/renderer/src/components/Sidebar.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/app/src/renderer/src/main.js b/app/src/renderer/src/main.js index 0ac3a5f..43d27f6 100644 --- a/app/src/renderer/src/main.js +++ b/app/src/renderer/src/main.js @@ -1,6 +1,5 @@ -import './assets/main.css' - import { createApp } from 'vue' import App from './App.vue' -createApp(App).mount('#app') +const app = createApp(App) +app.mount('#app') diff --git a/app/src/renderer/src/stores/mqtt.js b/app/src/renderer/src/stores/mqtt.js new file mode 100644 index 0000000..b5323d8 --- /dev/null +++ b/app/src/renderer/src/stores/mqtt.js @@ -0,0 +1,238 @@ +import { ref, computed } from 'vue' + +// 全局状态(单例) +const servers = ref([]) +const activeServerId = ref(null) +const messages = ref([]) +const publishHistory = ref([]) +const loading = ref(false) + +// 清理函数集合 +let cleanupFns = [] + +function setupListeners() { + // 清理旧监听器 + cleanupFns.forEach((fn) => fn()) + cleanupFns = [] + + cleanupFns.push( + window.api.onStatusChange((data) => { + const srv = servers.value.find((s) => s.id === data.id) + if (srv) { + srv.status = data.status + if (data.status === 'disconnected') { + srv.topics.forEach((t) => { + t.subscribed = false + }) + } + } + }) + ) + + cleanupFns.push( + window.api.onMessage((data) => { + if (data.serverId === activeServerId.value) { + messages.value.unshift(data.message) + if (messages.value.length > 500) messages.value.pop() + } + }) + ) + + cleanupFns.push( + window.api.onSubscriptionChange((data) => { + if (data.serverId === activeServerId.value) { + const srv = servers.value.find((s) => s.id === data.serverId) + if (srv) { + const t = srv.topics.find((t) => t.topic === data.topic || t.id === data.topicId) + if (t) { + t.subscribed = data.subscribed + if (data.topic && data.topic !== t.topic) { + t.topic = data.topic + } + } + } + } + }) + ) +} + +export function useMqttStore() { + const activeServer = computed( + () => servers.value.find((s) => s.id === activeServerId.value) || null + ) + + const connectedServers = computed(() => servers.value.filter((s) => s.status === 'connected')) + + const totalTopics = computed(() => + servers.value.reduce((sum, s) => sum + (s.topics?.length || 0), 0) + ) + + const subscribedTopics = computed(() => + servers.value.reduce((sum, s) => sum + (s.topics?.filter((t) => t.subscribed)?.length || 0), 0) + ) + + const totalMessages = computed(() => messages.value.length) + + // ==================== 初始化 ==================== + async function init() { + loading.value = true + try { + const data = await window.api.getServers() + servers.value = data + if (data.length > 0 && !activeServerId.value) { + activeServerId.value = data[0].id + } + setupListeners() + } catch (e) { + console.error('初始化失败:', e) + } finally { + loading.value = false + } + } + + // ==================== 服务器管理 ==================== + function setActiveServer(id) { + activeServerId.value = id + // 切换时刷新消息 + refreshMessages() + refreshPublishHistory() + } + + async function addServer(config) { + const server = await window.api.addServer(config) + servers.value.push(server) + if (!activeServerId.value) { + activeServerId.value = server.id + } + return server + } + + async function updateServer(id, config) { + const result = await window.api.updateServer(id, config) + if (result) { + const idx = servers.value.findIndex((s) => s.id === id) + if (idx !== -1) { + servers.value[idx] = { ...servers.value[idx], ...result } + } + } + return result + } + + async function deleteServer(id) { + await window.api.deleteServer(id) + servers.value = servers.value.filter((s) => s.id !== id) + if (activeServerId.value === id) { + activeServerId.value = servers.value.length > 0 ? servers.value[0].id : null + } + } + + async function connectServer(id) { + return await window.api.connect(id) + } + + async function disconnectServer(id) { + await window.api.disconnect(id) + } + + // ==================== 主题管理 ==================== + async function addTopic(serverId, topic, qos) { + const result = await window.api.addTopic(serverId, topic, qos) + if (result) { + const srv = servers.value.find((s) => s.id === serverId) + if (srv) { + srv.topics.push({ topic, qos, subscribed: false }) + } + } + return result + } + + async function removeTopic(serverId, topic) { + await window.api.removeTopic(serverId, topic) + const srv = servers.value.find((s) => s.id === serverId) + if (srv) { + srv.topics = srv.topics.filter((t) => t.topic !== topic) + } + } + + async function subscribeTopic(serverId, topic) { + return await window.api.subscribe(serverId, topic) + } + + async function unsubscribeTopic(serverId, topic) { + return await window.api.unsubscribe(serverId, topic) + } + + async function subscribeAllTopics(serverId) { + return await window.api.subscribeAll(serverId) + } + + async function updateTopic(serverId, topicId, topic, qos) { + return await window.api.updateTopic(serverId, topicId, topic, qos) + } + + // ==================== 消息发布 ==================== + async function publishMessage(serverId, topic, payload, opts) { + const result = await window.api.publish(serverId, topic, payload, opts) + if (result && serverId === activeServerId.value) { + refreshPublishHistory() + } + return result + } + + // ==================== 消息管理 ==================== + async function refreshMessages() { + if (!activeServerId.value) return + messages.value = await window.api.getMessages(activeServerId.value) + } + + async function refreshPublishHistory() { + if (!activeServerId.value) return + publishHistory.value = await window.api.getPublishHistory(activeServerId.value) + } + + async function clearMessages() { + if (!activeServerId.value) return + await window.api.clearMessages(activeServerId.value) + messages.value = [] + } + + async function clearPublishHistory() { + if (!activeServerId.value) return + await window.api.clearPublishHistory(activeServerId.value) + publishHistory.value = [] + } + + return { + // 状态 + servers, + activeServerId, + activeServer, + messages, + publishHistory, + loading, + // 计算属性 + connectedServers, + totalTopics, + subscribedTopics, + totalMessages, + // 方法 + init, + setActiveServer, + addServer, + updateServer, + deleteServer, + connectServer, + disconnectServer, + addTopic, + removeTopic, + subscribeTopic, + unsubscribeTopic, + subscribeAllTopics, + updateTopic, + publishMessage, + refreshMessages, + refreshPublishHistory, + clearMessages, + clearPublishHistory + } +} diff --git a/app/src/renderer/src/views/Dashboard.vue b/app/src/renderer/src/views/Dashboard.vue new file mode 100644 index 0000000..9521692 --- /dev/null +++ b/app/src/renderer/src/views/Dashboard.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/app/src/renderer/src/views/Messages.vue b/app/src/renderer/src/views/Messages.vue new file mode 100644 index 0000000..89f8a6d --- /dev/null +++ b/app/src/renderer/src/views/Messages.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/app/src/renderer/src/views/Publish.vue b/app/src/renderer/src/views/Publish.vue new file mode 100644 index 0000000..ee410f7 --- /dev/null +++ b/app/src/renderer/src/views/Publish.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/app/src/renderer/src/views/Subscribe.vue b/app/src/renderer/src/views/Subscribe.vue new file mode 100644 index 0000000..611febb --- /dev/null +++ b/app/src/renderer/src/views/Subscribe.vue @@ -0,0 +1,344 @@ + + + + +