From 2bf5fd1a374fd61c52e4368b3d600c32aaf17b2e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 04:58:29 +0800 Subject: [PATCH] =?UTF-8?q?feat=E2=9C=A8:=20=E6=96=B0=E5=A2=9EWeb=20UI?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=8A=E6=95=B0=E6=8D=AE=E5=BA=93=E3=80=81?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=AD=89API=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 210 ++++++- pyproject.toml | 2 + zhenxun/configs/utils/__init__.py | 2 + zhenxun/models/fg_request.py | 48 +- zhenxun/plugins/web_ui/__init__.py | 76 +++ zhenxun/plugins/web_ui/api/__init__.py | 1 + zhenxun/plugins/web_ui/api/logs/__init__.py | 1 + .../plugins/web_ui/api/logs/log_manager.py | 35 ++ zhenxun/plugins/web_ui/api/logs/logs.py | 40 ++ zhenxun/plugins/web_ui/api/tabs/__init__.py | 5 + .../web_ui/api/tabs/database/__init__.py | 121 ++++ .../web_ui/api/tabs/database/models/model.py | 24 + .../api/tabs/database/models/sql_log.py | 37 ++ .../plugins/web_ui/api/tabs/main/__init__.py | 290 ++++++++++ .../web_ui/api/tabs/main/data_source.py | 35 ++ zhenxun/plugins/web_ui/api/tabs/main/model.py | 105 ++++ .../web_ui/api/tabs/manage/__init__.py | 529 ++++++++++++++++++ .../plugins/web_ui/api/tabs/manage/model.py | 265 +++++++++ .../web_ui/api/tabs/plugin_manage/__init__.py | 187 +++++++ .../web_ui/api/tabs/plugin_manage/model.py | 125 +++++ .../web_ui/api/tabs/system/__init__.py | 121 ++++ .../plugins/web_ui/api/tabs/system/model.py | 64 +++ zhenxun/plugins/web_ui/auth/__init__.py | 47 ++ zhenxun/plugins/web_ui/base_model.py | 108 ++++ zhenxun/plugins/web_ui/config.py | 36 ++ zhenxun/plugins/web_ui/utils.py | 136 +++++ zhenxun/utils/enum.py | 2 + zhenxun/utils/plugin_models/base.py | 9 + 28 files changed, 2643 insertions(+), 18 deletions(-) create mode 100644 zhenxun/plugins/web_ui/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/logs/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/logs/log_manager.py create mode 100644 zhenxun/plugins/web_ui/api/logs/logs.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/database/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/database/models/model.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/main/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/main/data_source.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/main/model.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/manage/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/manage/model.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/system/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/system/model.py create mode 100644 zhenxun/plugins/web_ui/auth/__init__.py create mode 100644 zhenxun/plugins/web_ui/base_model.py create mode 100644 zhenxun/plugins/web_ui/config.py create mode 100644 zhenxun/plugins/web_ui/utils.py create mode 100644 zhenxun/utils/plugin_models/base.py diff --git a/poetry.lock b/poetry.lock index e00d88b1..53a0678d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -591,6 +591,75 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "chardet" version = "5.2.0" @@ -792,6 +861,60 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "cryptography" +version = "43.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, + {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, + {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, + {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, + {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, + {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, + {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "dateparser" version = "1.2.0" @@ -835,6 +958,29 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "ecdsa" +version = "0.19.0" +description = "ECDSA cryptographic signature library (pure python)" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, + {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, +] + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "emoji" version = "2.10.1" @@ -2513,6 +2659,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "pydantic" version = "1.10.14" @@ -2737,6 +2899,33 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" +optional = false +python-versions = "*" +files = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] +pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "python-markdown-math" version = "0.8" @@ -2756,6 +2945,25 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "python-multipart" +version = "0.0.9" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, +] + +[package.extras] +dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "python-slugify" version = "8.0.2" @@ -4137,4 +4345,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3a23c97b954472fca2124960e82ba695c52f3448bec4458df94b3d884d23c1e4" +content-hash = "1069f396df7f09336b9ea7737997061e4dfea458a561995a2afee74fd9cf36ad" diff --git a/pyproject.toml b/pyproject.toml index cbe43480..8faaa329 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,8 @@ aiohttp = "^3.9.5" dateparser = "^1.2.0" nonebot-plugin-alconna = "^0.50.2" bilireq = "0.2.3post0" +python-jose = {extras = ["cryptography"], version = "^3.3.0"} +python-multipart = "^0.0.9" [tool.poetry.dev-dependencies] diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index 4a192b3d..d9f4367e 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -168,6 +168,8 @@ class PluginExtraData(BaseModel): """超级用户帮助""" aliases: Set[str] = set() """额外名称""" + sql_list: list[str] | None = None + """常用sql""" class NoSuchConfig(Exception): diff --git a/zhenxun/models/fg_request.py b/zhenxun/models/fg_request.py index ad740eff..43cbfdbc 100644 --- a/zhenxun/models/fg_request.py +++ b/zhenxun/models/fg_request.py @@ -9,7 +9,9 @@ from zhenxun.utils.exception import NotFoundError class FgRequest(Model): id = fields.IntField(pk=True, generated=True, auto_increment=True) """自增id""" - request_type = fields.CharEnumField(RequestType, default=None, description="请求类型") + request_type = fields.CharEnumField( + RequestType, default=None, description="请求类型" + ) """请求类型""" platform = fields.CharField(255, description="平台") """平台""" @@ -25,7 +27,9 @@ class FgRequest(Model): """对象名称""" comment = fields.CharField(max_length=255, null=True, description="验证信息") """验证信息""" - handle_type = fields.CharEnumField(RequestHandleType, null=True, description="处理类型") + handle_type = fields.CharEnumField( + RequestHandleType, null=True, description="处理类型" + ) """处理类型""" class Meta: @@ -33,53 +37,61 @@ class FgRequest(Model): table_description = "好友群组请求" @classmethod - async def approve(cls, bot: Bot, id: int, request_type: RequestType): + async def approve(cls, bot: Bot, id: int): """同意请求 参数: bot: Bot id: 请求id - request_type: 请求类型 异常: NotFoundError: 未发现请求 """ - await cls._handle_request(bot, id, request_type, RequestHandleType.APPROVE) + await cls._handle_request(bot, id, RequestHandleType.APPROVE) @classmethod - async def refused(cls, bot: Bot, id: int, request_type: RequestType): + async def refused(cls, bot: Bot, id: int): """拒绝请求 参数: bot: Bot id: 请求id - request_type: 请求类型 异常: NotFoundError: 未发现请求 """ - await cls._handle_request(bot, id, request_type, RequestHandleType.REFUSED) + await cls._handle_request(bot, id, RequestHandleType.REFUSED) @classmethod - async def ignore(cls, bot: Bot, id: int, request_type: RequestType): + async def ignore(cls, id: int): + """忽略请求 + + 参数: + id: 请求id + + 异常: + NotFoundError: 未发现请求 + """ + await cls._handle_request(None, id, RequestHandleType.IGNORE) + + @classmethod + async def expire(cls, id: int): """忽略请求 参数: bot: Bot id: 请求id - request_type: 请求类型 异常: NotFoundError: 未发现请求 """ - await cls._handle_request(bot, id, request_type, RequestHandleType.IGNORE) + await cls._handle_request(None, id, RequestHandleType.EXPIRE) @classmethod async def _handle_request( cls, - bot: Bot, + bot: Bot | None, id: int, - request_type: RequestType, handle_type: RequestHandleType, ): """处理请求 @@ -87,19 +99,21 @@ class FgRequest(Model): 参数: bot: Bot id: 请求id - request_type: 请求类型 handle_type: 处理类型 异常: NotFoundError: 未发现请求 """ - req = await cls.get_or_none(id=id, request_type=request_type) + req = await cls.get_or_none(id=id) if not req: raise NotFoundError req.handle_type = RequestHandleType await req.save(update_fields=["handle_type"]) - if handle_type != RequestHandleType.IGNORE: - if request_type == RequestType.FRIEND: + if bot and handle_type not in [ + RequestHandleType.IGNORE, + RequestHandleType.EXPIRE, + ]: + if req.request_type == RequestType.FRIEND: await bot.set_friend_add_request( flag=req.flag, approve=handle_type == RequestHandleType.APPROVE ) diff --git a/zhenxun/plugins/web_ui/__init__.py b/zhenxun/plugins/web_ui/__init__.py new file mode 100644 index 00000000..37684372 --- /dev/null +++ b/zhenxun/plugins/web_ui/__init__.py @@ -0,0 +1,76 @@ +import asyncio + +import nonebot +from fastapi import APIRouter, FastAPI +from nonebot.adapters.onebot.v11 import Bot, MessageEvent +from nonebot.log import default_filter, default_format +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.typing import T_State + +from zhenxun.configs.config import Config as gConfig +from zhenxun.services.log import logger, logger_ + +from .api.logs import router as ws_log_routes +from .api.logs.log_manager import LOG_STORAGE +from .api.tabs.database import router as database_router +from .api.tabs.main import router as main_router +from .api.tabs.main import ws_router as status_routes +from .api.tabs.manage import router as manage_router +from .api.tabs.manage import ws_router as chat_routes +from .api.tabs.plugin_manage import router as plugin_router +from .api.tabs.system import router as system_router +from .auth import router as auth_router + +driver = nonebot.get_driver() + +gConfig.add_plugin_config("web-ui", "username", "admin", help="前端管理用户名") + +gConfig.add_plugin_config("web-ui", "password", None, help="前端管理密码") + +gConfig.set_name("web-ui", "web-ui") + + +BaseApiRouter = APIRouter(prefix="/zhenxun/api") + + +BaseApiRouter.include_router(auth_router) +BaseApiRouter.include_router(main_router) +BaseApiRouter.include_router(manage_router) +BaseApiRouter.include_router(database_router) +BaseApiRouter.include_router(plugin_router) +BaseApiRouter.include_router(system_router) + + +WsApiRouter = APIRouter(prefix="/zhenxun/socket") + +WsApiRouter.include_router(ws_log_routes) +WsApiRouter.include_router(status_routes) +WsApiRouter.include_router(chat_routes) + + +@driver.on_startup +def _(): + try: + + async def log_sink(message: str): + loop = None + if not loop: + try: + loop = asyncio.get_running_loop() + except Exception as e: + logger.warning("Web Ui log_sink", e=e) + if not loop: + loop = asyncio.new_event_loop() + loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))) + + logger_.add( + log_sink, colorize=True, filter=default_filter, format=default_format + ) + + app: FastAPI = nonebot.get_app() + app.include_router(BaseApiRouter) + app.include_router(WsApiRouter) + logger.info("API启动成功", "Web UI") + except Exception as e: + logger.error("API启动失败", "Web UI", e=e) diff --git a/zhenxun/plugins/web_ui/api/__init__.py b/zhenxun/plugins/web_ui/api/__init__.py new file mode 100644 index 00000000..32d31b27 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/__init__.py @@ -0,0 +1 @@ +from .tabs import * diff --git a/zhenxun/plugins/web_ui/api/logs/__init__.py b/zhenxun/plugins/web_ui/api/logs/__init__.py new file mode 100644 index 00000000..d6684888 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/logs/__init__.py @@ -0,0 +1 @@ +from .logs import * diff --git a/zhenxun/plugins/web_ui/api/logs/log_manager.py b/zhenxun/plugins/web_ui/api/logs/log_manager.py new file mode 100644 index 00000000..71992c91 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/logs/log_manager.py @@ -0,0 +1,35 @@ +import asyncio +from typing import Awaitable, Callable, Generic, TypeVar + +PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" + +_T = TypeVar("_T") +LogListener = Callable[[_T], Awaitable[None]] + + +class LogStorage(Generic[_T]): + """ + 日志存储 + """ + + def __init__(self, rotation: float = 5 * 60): + self.count, self.rotation = 0, rotation + self.logs: dict[int, str] = {} + self.listeners: set[LogListener[str]] = set() + + async def add(self, log: str): + seq = self.count = self.count + 1 + self.logs[seq] = log + asyncio.get_running_loop().call_later(self.rotation, self.remove, seq) + await asyncio.gather( + *map(lambda listener: listener(log), self.listeners), + return_exceptions=True, + ) + return seq + + def remove(self, seq: int): + del self.logs[seq] + return + + +LOG_STORAGE: LogStorage[str] = LogStorage[str]() diff --git a/zhenxun/plugins/web_ui/api/logs/logs.py b/zhenxun/plugins/web_ui/api/logs/logs.py new file mode 100644 index 00000000..01c78096 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/logs/logs.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, WebSocket +from loguru import logger +from nonebot.utils import escape_tag +from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState + +from .log_manager import LOG_STORAGE + +router = APIRouter() + + +@router.get("/logs", response_model=list[str]) +async def system_logs_history(reverse: bool = False): + """历史日志 + + 参数: + reverse: 反转顺序. + """ + return LOG_STORAGE.list(reverse=reverse) # type: ignore + + +@router.websocket("/logs") +async def system_logs_realtime(websocket: WebSocket): + await websocket.accept() + + async def log_listener(log: str): + await websocket.send_text(log) + + LOG_STORAGE.listeners.add(log_listener) + try: + while websocket.client_state == WebSocketState.CONNECTED: + recv = await websocket.receive() + logger.trace( + f"{system_logs_realtime.__name__!r} received " + f"{escape_tag(repr(recv))}" + ) + except WebSocketDisconnect: + pass + finally: + LOG_STORAGE.listeners.remove(log_listener) + return diff --git a/zhenxun/plugins/web_ui/api/tabs/__init__.py b/zhenxun/plugins/web_ui/api/tabs/__init__.py new file mode 100644 index 00000000..99ed6ea1 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/__init__.py @@ -0,0 +1,5 @@ +from .database import * +from .main import * +from .manage import * +from .plugin_manage import * +from .system import * diff --git a/zhenxun/plugins/web_ui/api/tabs/database/__init__.py b/zhenxun/plugins/web_ui/api/tabs/database/__init__.py new file mode 100644 index 00000000..2fd77085 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/database/__init__.py @@ -0,0 +1,121 @@ +import nonebot +from fastapi import APIRouter, Request +from nonebot.drivers import Driver +from tortoise import Tortoise +from tortoise.exceptions import OperationalError + +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.services.db_context import TestSQL + +from ....base_model import BaseResultModel, QueryModel, Result +from ....utils import authentication +from .models.model import SqlModel, SqlText +from .models.sql_log import SqlLog + +router = APIRouter(prefix="/database") + + +driver: Driver = nonebot.get_driver() + + +SQL_DICT = {} + + +SELECT_TABLE_SQL = """ +select a.tablename as name,d.description as desc from pg_tables a + left join pg_class c on relname=tablename + left join pg_description d on oid=objoid and objsubid=0 where a.schemaname = 'public' +""" + +SELECT_TABLE_COLUMN_SQL = """ +SELECT column_name, data_type, character_maximum_length as max_length, is_nullable +FROM information_schema.columns +WHERE table_name = '{}'; +""" + + +@driver.on_startup +async def _(): + for plugin in nonebot.get_loaded_plugins(): + module = plugin.name + sql_list = [] + if plugin.metadata and plugin.metadata.extra: + sql_list = plugin.metadata.extra.get("sql_list") + if module in SQL_DICT: + raise ValueError(f"{module} 常用SQL module 重复") + if sql_list: + SqlModel( + name="", + module=module, + sql_list=sql_list, + ) + SQL_DICT[module] = SqlModel + if SQL_DICT: + result = await PluginInfo.filter(module__in=SQL_DICT.keys()).values_list( + "module", "name" + ) + module2name = {r[0]: r[1] for r in result} + for s in SQL_DICT: + module = SQL_DICT[s].module + if module in module2name: + SQL_DICT[s].name = module2name[module] + else: + SQL_DICT[s].name = module + + +@router.get( + "/get_table_list", dependencies=[authentication()], description="获取数据库表" +) +async def _() -> Result: + db = Tortoise.get_connection("default") + query = await db.execute_query_dict(SELECT_TABLE_SQL) + return Result.ok(query) + + +@router.get( + "/get_table_column", dependencies=[authentication()], description="获取表字段" +) +async def _(table_name: str) -> Result: + db = Tortoise.get_connection("default") + print(SELECT_TABLE_COLUMN_SQL.format(table_name)) + query = await db.execute_query_dict(SELECT_TABLE_COLUMN_SQL.format(table_name)) + return Result.ok(query) + + +@router.post("/exec_sql", dependencies=[authentication()], description="执行sql") +async def _(sql: SqlText, request: Request) -> Result: + ip = request.client.host if request.client else "unknown" + try: + if sql.sql.lower().startswith("select"): + db = Tortoise.get_connection("default") + res = await db.execute_query_dict(sql.sql) + await SqlLog.add(ip or "0.0.0.0", sql.sql, "") + return Result.ok(res, "执行成功啦!") + else: + result = await TestSQL.raw(sql.sql) + await SqlLog.add(ip or "0.0.0.0", sql.sql, str(result)) + return Result.ok(info="执行成功啦!") + except OperationalError as e: + await SqlLog.add(ip or "0.0.0.0", sql.sql, str(e), False) + return Result.warning_(f"sql执行错误: {e}") + + +@router.post("/get_sql_log", dependencies=[authentication()], description="sql日志列表") +async def _(query: QueryModel) -> Result: + total = await SqlLog.all().count() + if total % query.size: + total += 1 + data = ( + await SqlLog.all() + .order_by("-id") + .offset((query.index - 1) * query.size) + .limit(query.size) + ) + return Result.ok(BaseResultModel(total=total, data=data)) + + +@router.get("/get_common_sql", dependencies=[authentication()], description="常用sql") +async def _(plugin_name: str | None = None) -> Result: + if plugin_name: + return Result.ok(SQL_DICT.get(plugin_name)) + return Result.ok(str(SQL_DICT)) diff --git a/zhenxun/plugins/web_ui/api/tabs/database/models/model.py b/zhenxun/plugins/web_ui/api/tabs/database/models/model.py new file mode 100644 index 00000000..e18e4cfb --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/database/models/model.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel + +from zhenxun.utils.plugin_models.base import CommonSql + + +class SqlText(BaseModel): + """ + sql语句 + """ + + sql: str + + +class SqlModel(BaseModel): + """ + 常用sql + """ + + name: str + """插件中文名称""" + module: str + """插件名称""" + sql_list: list[CommonSql] + """插件列表""" diff --git a/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py b/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py new file mode 100644 index 00000000..691f1b5a --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py @@ -0,0 +1,37 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class SqlLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + ip = fields.CharField(255) + """ip""" + sql = fields.CharField(255) + """sql""" + result = fields.CharField(255, null=True) + """结果""" + is_suc = fields.BooleanField(default=True) + """是否成功""" + create_time = fields.DatetimeField(auto_now_add=True) + """创建时间""" + + class Meta: + table = "sql_log" + table_description = "sql执行日志" + + @classmethod + async def add( + cls, ip: str, sql: str, result: str | None = None, is_suc: bool = True + ): + """获取用户在群内的等级 + + 参数: + ip: ip + sql: sql + result: 返回结果 + is_suc: 是否成功 + """ + await cls.create(ip=ip, sql=sql, result=result, is_suc=is_suc) diff --git a/zhenxun/plugins/web_ui/api/tabs/main/__init__.py b/zhenxun/plugins/web_ui/api/tabs/main/__init__.py new file mode 100644 index 00000000..ed8bb576 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/main/__init__.py @@ -0,0 +1,290 @@ +import asyncio +import time +from datetime import datetime, timedelta +from pathlib import Path + +import nonebot +from fastapi import APIRouter, WebSocket +from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState +from tortoise.functions import Count +from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK + +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.group_info import GroupInfo +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils + +from ....base_model import Result +from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType +from ....utils import authentication, get_system_status +from .data_source import bot_live +from .model import ActiveGroup, BaseInfo, ChatHistoryCount, HotPlugin + +run_time = time.time() + +ws_router = APIRouter() +router = APIRouter(prefix="/main") + + +@router.get("/get_base_info", dependencies=[authentication()], description="基础信息") +async def _(bot_id: str | None = None) -> Result: + """获取Bot基础信息 + + 参数: + bot_id (Optional[str], optional): bot_id. Defaults to None. + + 返回: + Result: 获取指定bot信息与bot列表 + """ + bot_list: list[BaseInfo] = [] + if bots := nonebot.get_bots(): + select_bot: BaseInfo + for key, bot in bots.items(): + login_info = await bot.get_login_info() + bot_list.append( + BaseInfo( + bot=bot, # type: ignore + self_id=bot.self_id, + nickname=login_info["nickname"], + ava_url=AVA_URL.format(bot.self_id), + ) + ) + # 获取指定qq号的bot信息,若无指定 则获取第一个 + if _bl := [b for b in bot_list if b.self_id == bot_id]: + select_bot = _bl[0] + else: + select_bot = bot_list[0] + select_bot.is_select = True + select_bot.config = select_bot.bot.config + now = datetime.now() + # 今日累计接收消息 + select_bot.received_messages = await ChatHistory.filter( + bot_id=select_bot.self_id, + create_time__gte=now - timedelta(hours=now.hour), + ).count() + # 群聊数量 + select_bot.group_count = len(await select_bot.bot.get_group_list()) + # 好友数量 + select_bot.friend_count = len(await select_bot.bot.get_friend_list()) + for bot in bot_list: + bot.bot = None # type: ignore + # 插件加载数量 + select_bot.plugin_count = await PluginInfo.all().count() + fail_count = await PluginInfo.filter(load_status=False).count() + select_bot.fail_plugin_count = fail_count + select_bot.success_plugin_count = ( + select_bot.plugin_count - select_bot.fail_plugin_count + ) + # 连接时间 + select_bot.connect_time = bot_live.get(select_bot.self_id) or 0 + if select_bot.connect_time: + connect_date = datetime.fromtimestamp(select_bot.connect_time) + connect_date_str = connect_date.strftime("%Y-%m-%d %H:%M:%S") + select_bot.connect_date = datetime.strptime( + connect_date_str, "%Y-%m-%d %H:%M:%S" + ) + version_file = Path() / "__version__" + if version_file.exists(): + if text := version_file.open().read(): + if ver := text.replace("__version__: ", "").strip(): + select_bot.version = ver + day_call = await Statistics.filter( + create_time__gte=now - timedelta(hours=now.hour) + ).count() + select_bot.day_call = day_call + return Result.ok(bot_list, "拿到信息啦!") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_all_ch_count", dependencies=[authentication()], description="获取接收消息数量" +) +async def _(bot_id: str) -> Result: + now = datetime.now() + all_count = await ChatHistory.filter(bot_id=bot_id).count() + day_count = await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(hours=now.hour) + ).count() + week_count = await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=7) + ).count() + month_count = await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=30) + ).count() + year_count = await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=365) + ).count() + return Result.ok( + ChatHistoryCount( + num=all_count, + day=day_count, + week=week_count, + month=month_count, + year=year_count, + ) + ) + + +@router.get( + "/get_ch_count", dependencies=[authentication()], description="获取接收消息数量" +) +async def _(bot_id: str, query_type: QueryDateType | None = None) -> Result: + if bots := nonebot.get_bots(): + if not query_type: + return Result.ok(await ChatHistory.filter(bot_id=bot_id).count()) + now = datetime.now() + if query_type == QueryDateType.DAY: + return Result.ok( + await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(hours=now.hour) + ).count() + ) + if query_type == QueryDateType.WEEK: + return Result.ok( + await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=7) + ).count() + ) + if query_type == QueryDateType.MONTH: + return Result.ok( + await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=30) + ).count() + ) + if query_type == QueryDateType.YEAR: + return Result.ok( + await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=365) + ).count() + ) + return Result.warning_("无Bot连接...") + + +@router.get( + "get_fg_count", dependencies=[authentication()], description="好友/群组数量" +) +async def _(bot_id: str) -> Result: + if bots := nonebot.get_bots(): + if bot_id not in bots: + return Result.warning_("指定Bot未连接...") + bot = bots[bot_id] + platform = PlatformUtils.get_platform(bot) + if platform == "qq": + data = { + "friend_count": len(await bot.get_friend_list()), + "group_count": len(await bot.get_group_list()), + } + return Result.ok(data) + return Result.warning_("暂不支持该平台...") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_run_time", dependencies=[authentication()], description="获取nb运行时间" +) +async def _() -> Result: + return Result.ok(int(time.time() - run_time)) + + +@router.get( + "/get_active_group", dependencies=[authentication()], description="获取活跃群聊" +) +async def _(date_type: QueryDateType | None = None) -> Result: + query = ChatHistory + now = datetime.now() + if date_type == QueryDateType.DAY: + query = ChatHistory.filter(create_time__gte=now - timedelta(hours=now.hour)) + if date_type == QueryDateType.WEEK: + query = ChatHistory.filter(create_time__gte=now - timedelta(days=7)) + if date_type == QueryDateType.MONTH: + query = ChatHistory.filter(create_time__gte=now - timedelta(days=30)) + if date_type == QueryDateType.YEAR: + query = ChatHistory.filter(create_time__gte=now - timedelta(days=365)) + data_list = ( + await query.annotate(count=Count("id")) + .filter(group_id__not_isnull=True) + .group_by("group_id") + .order_by("-count") + .limit(5) + .values_list("group_id", "count") + ) + active_group_list = [] + id2name = {} + if data_list: + if info_list := await GroupInfo.filter( + group_id__in=[x[0] for x in data_list] + ).all(): + for group_info in info_list: + id2name[group_info.group_id] = group_info.group_name + for data in data_list: + active_group_list.append( + ActiveGroup( + group_id=data[0], + name=id2name.get(data[0]) or data[0], + chat_num=data[1], + ava_img=GROUP_AVA_URL.format(data[0], data[0]), + ) + ) + active_group_list = sorted( + active_group_list, key=lambda x: x.chat_num, reverse=True + ) + if len(active_group_list) > 5: + active_group_list = active_group_list[:5] + return Result.ok(active_group_list) + + +@router.get( + "/get_hot_plugin", dependencies=[authentication()], description="获取热门插件" +) +async def _(date_type: QueryDateType | None = None) -> Result: + query = Statistics + now = datetime.now() + if date_type == QueryDateType.DAY: + query = Statistics.filter(create_time__gte=now - timedelta(hours=now.hour)) + if date_type == QueryDateType.WEEK: + query = Statistics.filter(create_time__gte=now - timedelta(days=7)) + if date_type == QueryDateType.MONTH: + query = Statistics.filter(create_time__gte=now - timedelta(days=30)) + if date_type == QueryDateType.YEAR: + query = Statistics.filter(create_time__gte=now - timedelta(days=365)) + data_list = ( + await query.annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + hot_plugin_list = [] + module_list = [x[0] for x in data_list] + plugins = await PluginInfo.filter(module__in=module_list).all() + module2name = {p.module: p.name for p in plugins} + for data in data_list: + module = data[0] + name = module2name.get(module) or module + hot_plugin_list.append( + HotPlugin( + module=data[0], + name=name, + count=data[1], + ) + ) + hot_plugin_list = sorted(hot_plugin_list, key=lambda x: x.count, reverse=True) + if len(hot_plugin_list) > 5: + hot_plugin_list = hot_plugin_list[:5] + return Result.ok(hot_plugin_list) + + +@ws_router.websocket("/system_status") +async def system_logs_realtime(websocket: WebSocket, sleep: int = 5): + await websocket.accept() + logger.debug("ws system_status is connect") + try: + while websocket.client_state == WebSocketState.CONNECTED: + system_status = await get_system_status() + await websocket.send_text(system_status.json()) + await asyncio.sleep(sleep) + except (WebSocketDisconnect, ConnectionClosedError, ConnectionClosedOK): + pass + return diff --git a/zhenxun/plugins/web_ui/api/tabs/main/data_source.py b/zhenxun/plugins/web_ui/api/tabs/main/data_source.py new file mode 100644 index 00000000..ca445016 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/main/data_source.py @@ -0,0 +1,35 @@ +import time + +import nonebot +from nonebot.adapters.onebot.v11 import Bot +from nonebot.drivers import Driver + +driver: Driver = nonebot.get_driver() + + +class BotLive: + def __init__(self): + self._data = {} + + def add(self, bot_id: str): + self._data[bot_id] = time.time() + + def get(self, bot_id: str) -> int | None: + return self._data.get(bot_id) + + def remove(self, bot_id: str): + if bot_id in self._data: + del self._data[bot_id] + + +bot_live = BotLive() + + +@driver.on_bot_connect +async def _(bot: Bot): + bot_live.add(bot.self_id) + + +@driver.on_bot_disconnect +async def _(bot: Bot): + bot_live.remove(bot.self_id) diff --git a/zhenxun/plugins/web_ui/api/tabs/main/model.py b/zhenxun/plugins/web_ui/api/tabs/main/model.py new file mode 100644 index 00000000..c9d76706 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/main/model.py @@ -0,0 +1,105 @@ +from datetime import datetime + +from nonebot.adapters import Bot +from nonebot.config import Config +from pydantic import BaseModel + + +class SystemStatus(BaseModel): + """ + 系统状态 + """ + + cpu: float + memory: float + disk: float + + +class BaseInfo(BaseModel): + """ + 基础信息 + """ + + bot: Bot + """Bot""" + self_id: str + """SELF ID""" + nickname: str + """昵称""" + ava_url: str + """头像url""" + friend_count: int = 0 + """好友数量""" + group_count: int = 0 + """群聊数量""" + received_messages: int = 0 + """今日 累计接收消息""" + connect_time: int = 0 + """连接时间""" + connect_date: datetime | None = None + """连接日期""" + + plugin_count: int = 0 + """加载插件数量""" + success_plugin_count: int = 0 + """加载成功插件数量""" + fail_plugin_count: int = 0 + """加载失败插件数量""" + + is_select: bool = False + """当前选择""" + + config: Config | None = None + """nb配置""" + day_call: int = 0 + """今日调用插件次数""" + version: str = "unknown" + """真寻版本""" + + class Config: + arbitrary_types_allowed = True + + +class ChatHistoryCount(BaseModel): + """ + 聊天记录数量 + """ + + num: int + """总数""" + day: int + """一天内""" + week: int + """一周内""" + month: int + """一月内""" + year: int + """一年内""" + + +class ActiveGroup(BaseModel): + """ + 活跃群聊数据 + """ + + group_id: str + """群组id""" + name: str + """群组名称""" + chat_num: int + """发言数量""" + ava_img: str + """群组头像""" + + +class HotPlugin(BaseModel): + """ + 热门插件 + """ + + module: str + """模块名""" + name: str + """插件名称""" + count: int + """调用次数""" diff --git a/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py new file mode 100644 index 00000000..82a34d56 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py @@ -0,0 +1,529 @@ +import re +from typing import Literal + +import nonebot +from fastapi import APIRouter +from nonebot.adapters.onebot.v11 import ActionFailed +from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState +from tortoise.functions import Count + +from zhenxun.configs.config import NICKNAME +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.fg_request import FgRequest +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import RequestHandleType, RequestType +from zhenxun.utils.exception import NotFoundError +from zhenxun.utils.platform import PlatformUtils + +from ....base_model import Result +from ....config import AVA_URL, GROUP_AVA_URL +from ....utils import authentication +from ...logs.log_manager import LOG_STORAGE +from .model import ( + DeleteFriend, + Friend, + FriendRequestResult, + GroupDetail, + GroupRequestResult, + GroupResult, + HandleRequest, + LeaveGroup, + Message, + MessageItem, + Plugin, + ReqResult, + SendMessage, + Task, + UpdateGroup, + UserDetail, +) + +ws_router = APIRouter() +router = APIRouter(prefix="/manage") + +SUB_PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" + +GROUP_PATTERN = r'.*?Message (-?\d*) from (\d*)@\[群:(\d*)] "(.*)"' + +PRIVATE_PATTERN = r'.*?Message (-?\d*) from (\d*) "(.*)"' + +AT_PATTERN = r"\[CQ:at,qq=(.*)\]" + +IMAGE_PATTERN = r"\[CQ:image,.*,url=(.*);.*?\]" + + +@router.get( + "/get_group_list", dependencies=[authentication()], description="获取群组列表" +) +async def _(bot_id: str) -> Result: + """ + 获取群信息 + """ + if bots := nonebot.get_bots(): + if bot_id not in bots: + return Result.warning_("指定Bot未连接...") + group_list_result = [] + try: + group_info = {} + group_list = await bots[bot_id].get_group_list() + for g in group_list: + gid = g["group_id"] + g["ava_url"] = GROUP_AVA_URL.format(gid, gid) + group_list_result.append(GroupResult(**g)) + except Exception as e: + logger.error("调用API错误", "/get_group_list", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(group_list_result, "拿到了新鲜出炉的数据!") + return Result.warning_("无Bot连接...") + + +@router.post( + "/update_group", dependencies=[authentication()], description="修改群组信息" +) +async def _(group: UpdateGroup) -> Result: + try: + group_id = group.group_id + if db_group := await GroupConsole.get_or_none(group_id=group_id): + db_group.level = group.level + db_group.status = group.status + if group.close_plugins: + db_group.block_plugin = ",".join(group.close_plugins) + "," + # TODO: 关闭task + await db_group.save(update_fields=["level", "status", "block_plugin"]) + except Exception as e: + logger.error("调用API错误", "/get_group", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(info="已完成记录!") + + +@router.get( + "/get_friend_list", dependencies=[authentication()], description="获取好友列表" +) +async def _(bot_id: str) -> Result: + """ + 获取群信息 + """ + if bots := nonebot.get_bots(): + if bot_id not in bots: + return Result.warning_("指定Bot未连接...") + try: + platform = PlatformUtils.get_platform(bots[bot_id]) + if platform != "qq": + return Result.warning_("该平台暂不支持该功能...") + friend_list = await bots[bot_id].get_friend_list() + for f in friend_list: + f["ava_url"] = AVA_URL.format(f["user_id"]) + return Result.ok( + [Friend(**f) for f in friend_list if str(f["user_id"]) != bot_id], + "拿到了新鲜出炉的数据!", + ) + except Exception as e: + logger.error("调用API错误", "/get_group_list", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_request_count", dependencies=[authentication()], description="获取请求数量" +) +async def _() -> Result: + f_count = await FgRequest.filter(request_type=RequestType.FRIEND).count() + g_count = await FgRequest.filter(request_type=RequestType.GROUP).count() + data = { + "friend_count": f_count, + "group_count": g_count, + } + return Result.ok(data, f"{NICKNAME}带来了最新的数据!") + + +@router.get( + "/get_request_list", dependencies=[authentication()], description="获取请求列表" +) +async def _() -> Result: + try: + req_result = ReqResult() + data_list = await FgRequest.filter(handle_type__not_isnull=True).all() + for req in data_list: + if req.request_type == RequestType.FRIEND: + req_result.friend.append( + FriendRequestResult( + oid=req.id, + bot_id=req.bot_id, + id=req.user_id, + flag=req.flag, + nickname=req.nickname, + comment=req.comment, + ava_url=AVA_URL.format(req.user_id), + type=str(req.request_type).lower(), + ) + ) + else: + req_result.group.append( + GroupRequestResult( + oid=req.id, + bot_id=req.bot_id, + id=req.user_id, + flag=req.flag, + nickname=req.nickname, + comment=req.comment, + ava_url=GROUP_AVA_URL.format(req.group_id, req.group_id), + type=str(req.request_type).lower(), + invite_group=req.group_id, + group_name=None, + ) + ) + req_result.friend.reverse() + req_result.group.reverse() + except Exception as e: + logger.error("调用API错误", "/get_request", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(req_result, f"{NICKNAME}带来了最新的数据!") + + +@router.delete( + "/clear_request", dependencies=[authentication()], description="清空请求列表" +) +async def _(request_type: Literal["private", "group"]) -> Result: + await FgRequest.filter(handle_type__not_isnull=True).update( + handle_type=RequestHandleType.IGNORE + ) + return Result.ok(info="成功清除了数据!") + + +@router.post("/refuse_request", dependencies=[authentication()], description="拒绝请求") +async def _(parma: HandleRequest) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = parma.bot_id + if bot_id not in nonebot.get_bots(): + return Result.warning_("指定Bot未连接...") + try: + await FgRequest.refused(bots[bot_id], parma.id) + except ActionFailed as e: + await FgRequest.expire(parma.id) + return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") + except NotFoundError: + return Result.warning_("未找到此Id请求...") + return Result.ok(info="成功处理了请求!") + return Result.warning_("无Bot连接...") + except Exception as e: + logger.error("调用API错误", "/refuse_request", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.post("/delete_request", dependencies=[authentication()], description="忽略请求") +async def _(parma: HandleRequest) -> Result: + await FgRequest.expire(parma.id) + return Result.ok(info="成功处理了请求!") + + +@router.post( + "/approve_request", dependencies=[authentication()], description="同意请求" +) +async def _(parma: HandleRequest) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = parma.bot_id + if bot_id not in nonebot.get_bots(): + return Result.warning_("指定Bot未连接...") + if parma.request_type == "group": + if req := await FgRequest.get_or_none(id=parma.id): + if group := await GroupConsole.get_or_none(group_id=req.group_id): + await group.update_or_create(group_flag=1) + else: + group_info = await bots[bot_id].get_group_info( + group_id=req.group_id + ) + await GroupConsole.update_or_create( + group_id=str(group_info["group_id"]), + defaults={ + "group_name": group_info["group_name"], + "max_member_count": group_info["max_member_count"], + "member_count": group_info["member_count"], + "group_flag": 1, + }, + ) + else: + return Result.warning_("未找到此Id请求...") + try: + await FgRequest.approve(bots[bot_id], parma.id) + return Result.ok(info="成功处理了请求!") + except ActionFailed as e: + await FgRequest.expire(parma.id) + return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") + return Result.warning_("无Bot连接...") + except Exception as e: + logger.error("调用API错误", "/approve_request", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.post("/leave_group", dependencies=[authentication()], description="退群") +async def _(param: LeaveGroup) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = param.bot_id + platform = PlatformUtils.get_platform(bots[bot_id]) + if platform != "qq": + return Result.warning_("该平台不支持退群操作...") + group_list = await bots[bot_id].get_group_list() + if param.group_id not in [str(g["group_id"]) for g in group_list]: + return Result.warning_("Bot未在该群聊中...") + await bots[bot_id].set_group_leave(group_id=param.group_id) + return Result.ok(info="成功处理了请求!") + return Result.warning_("无Bot连接...") + except Exception as e: + logger.error("调用API错误", "/leave_group", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.post("/delete_friend", dependencies=[authentication()], description="删除好友") +async def _(param: DeleteFriend) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = param.bot_id + platform = PlatformUtils.get_platform(bots[bot_id]) + if platform != "qq": + return Result.warning_("该平台不支持删除好友操作...") + friend_list = await bots[bot_id].get_friend_list() + if param.user_id not in [str(g["user_id"]) for g in friend_list]: + return Result.warning_("Bot未有其好友...") + await bots[bot_id].delete_friend(user_id=param.user_id) + return Result.ok(info="成功处理了请求!") + return Result.warning_("Bot未连接...") + except Exception as e: + logger.error("调用API错误", "/delete_friend", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.get( + "/get_friend_detail", dependencies=[authentication()], description="获取好友详情" +) +async def _(bot_id: str, user_id: str) -> Result: + if bots := nonebot.get_bots(): + if bot_id in bots: + if fd := [ + x + for x in await bots[bot_id].get_friend_list() + if str(x["user_id"]) == user_id + ]: + like_plugin_list = ( + await Statistics.filter(user_id=user_id) + .annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + like_plugin = {} + module_list = [x[0] for x in like_plugin_list] + plugins = await PluginInfo.filter(module__in=module_list).all() + module2name = {p.module: p.name for p in plugins} + for data in like_plugin_list: + name = module2name.get(data[0]) or data[0] + like_plugin[name] = data[1] + user = fd[0] + user_detail = UserDetail( + user_id=user_id, + ava_url=AVA_URL.format(user_id), + nickname=user["nickname"], + remark=user["remark"], + is_ban=await BanConsole.is_ban(user_id), + chat_count=await ChatHistory.filter(user_id=user_id).count(), + call_count=await Statistics.filter(user_id=user_id).count(), + like_plugin=like_plugin, + ) + return Result.ok(user_detail) + else: + return Result.warning_("未添加指定好友...") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_group_detail", dependencies=[authentication()], description="获取群组详情" +) +async def _(bot_id: str, group_id: str) -> Result: + if bots := nonebot.get_bots(): + if bot_id in bots: + group = await GroupConsole.get_or_none(group_id=group_id) + if not group: + return Result.warning_("指定群组未被收录...") + like_plugin_list = ( + await Statistics.filter(group_id=group_id) + .annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + like_plugin = {} + plugins = await PluginInfo.all() + module2name = {p.module: p.name for p in plugins} + for data in like_plugin_list: + name = module2name.get(data[0]) or data[0] + like_plugin[name] = data[1] + close_plugins = [] + if group.block_plugin: + for module in group.block_plugin.split(","): + module_ = module.replace(":super", "") + is_super_block = module.endswith(":super") + plugin = Plugin( + module=module_, + plugin_name=module, + is_super_block=is_super_block, + ) + plugin.plugin_name = module2name.get(module) or module + close_plugins.append(plugin) + all_task = await TaskInfo.annotate().values_list("module", "name") + task_module2name = {x[0]: x[1] for x in all_task} + task_list = [] + if group.block_task: + split_task = group.block_task.split(",") + for task in all_task: + task_list.append( + Task( + name=task[0], + zh_name=task_module2name.get(task[0]) or task[0], + status=task[0] not in split_task, + ) + ) + group_detail = GroupDetail( + group_id=group_id, + ava_url=GROUP_AVA_URL.format(group_id, group_id), + name=group.group_name, + member_count=group.member_count, + max_member_count=group.max_member_count, + chat_count=await ChatHistory.filter(group_id=group_id).count(), + call_count=await Statistics.filter(group_id=group_id).count(), + like_plugin=like_plugin, + level=group.level, + status=group.status, + close_plugins=close_plugins, + task=task_list, + ) + return Result.ok(group_detail) + else: + return Result.warning_("未添加指定群组...") + return Result.warning_("无Bot连接...") + + +@router.post( + "/send_message", dependencies=[authentication()], description="获取群组详情" +) +async def _(param: SendMessage) -> Result: + if bots := nonebot.get_bots(): + if param.bot_id in bots: + platform = PlatformUtils.get_platform(bots[param.bot_id]) + if platform != "qq": + return Result.warning_("暂不支持该平台...") + try: + if param.user_id: + await bots[param.bot_id].send_private_msg( + user_id=str(param.user_id), message=param.message + ) + else: + await bots[param.bot_id].send_group_msg( + group_id=str(param.group_id), message=param.message + ) + except Exception as e: + return Result.fail(str(e)) + return Result.ok("发送成功!") + return Result.warning_("指定Bot未连接...") + return Result.warning_("无Bot连接...") + + +MSG_LIST = [] + +ID2NAME = {} + + +async def message_handle( + sub_log: str, type: Literal["private", "group"] +) -> Message | None: + global MSG_LIST, ID2NAME + pattern = PRIVATE_PATTERN if type == "private" else GROUP_PATTERN + msg_id = None + uid = None + gid = None + msg = None + img_list = re.findall(IMAGE_PATTERN, sub_log) + if r := re.search(pattern, sub_log): + if type == "private": + msg_id = r.group(1) + uid = r.group(2) + msg = r.group(3) + if uid not in ID2NAME: + if user := await FriendUser.get_or_none(user_id=uid): + ID2NAME[uid] = user.user_name or user.nickname + else: + msg_id = r.group(1) + uid = r.group(2) + gid = r.group(3) + msg = r.group(4) + if gid not in ID2NAME: + if user := await GroupInfoUser.get_or_none(user_id=uid, group_id=gid): + ID2NAME[uid] = user.user_name or user.nickname + if at_list := re.findall(AT_PATTERN, msg): + user_list = await GroupInfoUser.filter( + user_id__in=at_list, group_id=gid + ).all() + id2name = {u.user_id: (u.user_name or u.nickname) for u in user_list} + for qq in at_list: + msg = re.sub(rf"\[CQ:at,qq={qq}\]", f"@{id2name[qq] or ''}", msg) + if msg_id in MSG_LIST: + return + MSG_LIST.append(msg_id) + messages = [] + if msg and uid: + rep = re.split(r"\[CQ:image.*\]", msg) + if img_list: + for i in range(len(rep)): + messages.append(MessageItem(type="text", msg=rep[i])) + if i < len(img_list): + messages.append(MessageItem(type="img", msg=img_list[i])) + else: + messages = [MessageItem(type="text", msg=x) for x in rep] + return Message( + object_id=uid if type == "private" else gid, # type: ignore + user_id=uid, + group_id=gid, + message=messages, + name=ID2NAME.get(uid) or "", + ava_url=AVA_URL.format(uid), + ) + return None + + +@ws_router.websocket("/chat") +async def _(websocket: WebSocket): + await websocket.accept() + + async def log_listener(log: str): + global MSG_LIST, ID2NAME + sub_log = re.sub(SUB_PATTERN, "", log) + img_list = re.findall(IMAGE_PATTERN, sub_log) + if "message.private.friend" in log: + if message := await message_handle(sub_log, "private"): + await websocket.send_json(message.dict()) + else: + if r := re.search(GROUP_PATTERN, sub_log): + if message := await message_handle(sub_log, "group"): + await websocket.send_json(message.dict()) + if len(MSG_LIST) > 30: + MSG_LIST = MSG_LIST[-1:] + + LOG_STORAGE.listeners.add(log_listener) + try: + while websocket.client_state == WebSocketState.CONNECTED: + recv = await websocket.receive() + except WebSocketDisconnect: + pass + finally: + LOG_STORAGE.listeners.remove(log_listener) + return diff --git a/zhenxun/plugins/web_ui/api/tabs/manage/model.py b/zhenxun/plugins/web_ui/api/tabs/manage/model.py new file mode 100644 index 00000000..a1e16bf4 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/manage/model.py @@ -0,0 +1,265 @@ +from typing import Literal + +from pydantic import BaseModel + + +class Group(BaseModel): + """ + 群组信息 + """ + + group_id: str + """群组id""" + group_name: str + """群组名称""" + member_count: int + """成员人数""" + max_member_count: int + """群组最大人数""" + + +class Task(BaseModel): + """ + 被动技能 + """ + + name: str + """被动名称""" + zh_name: str + """被动中文名称""" + status: bool + """状态""" + + +class Plugin(BaseModel): + """ + 插件 + """ + + module: str + """模块名""" + plugin_name: str + """中文名""" + is_super_block: bool + """是否超级用户禁用""" + + +class GroupResult(BaseModel): + """ + 群组返回数据 + """ + + group_id: str + """群组id""" + group_name: str + """群组名称""" + ava_url: str + """群组头像""" + + +class Friend(BaseModel): + """ + 好友数据 + """ + + user_id: str + """用户id""" + nickname: str = "" + """昵称""" + remark: str = "" + """备注""" + ava_url: str = "" + """头像url""" + + +class UpdateGroup(BaseModel): + """ + 更新群组信息 + """ + + group_id: str + """群号""" + status: bool + """状态""" + level: int + """群权限""" + task: list[str] + """被动状态""" + close_plugins: list[str] + """关闭插件""" + + +class FriendRequestResult(BaseModel): + """ + 好友/群组请求管理 + """ + + bot_id: str + """bot_id""" + oid: int + """排序""" + id: str + """id""" + flag: str + """flag""" + nickname: str | None + """昵称""" + comment: str | None + """备注信息""" + ava_url: str + """头像""" + type: str + """类型 private group""" + + +class GroupRequestResult(FriendRequestResult): + """ + 群聊邀请请求 + """ + + invite_group: str + """邀请群聊""" + group_name: str | None + """群聊名称""" + + +class HandleRequest(BaseModel): + """ + 操作请求接收数据 + """ + + bot_id: str | None = None + """bot_id""" + id: int + """数据id""" + request_type: Literal["private", "group"] + """类型""" + + +class LeaveGroup(BaseModel): + """ + 退出群聊 + """ + + bot_id: str + """bot_id""" + group_id: str + """群聊id""" + + +class DeleteFriend(BaseModel): + """ + 删除好友 + """ + + bot_id: str + """bot_id""" + user_id: str + """用户id""" + + +class ReqResult(BaseModel): + """ + 好友/群组请求列表 + """ + + friend: list[FriendRequestResult] = [] + """好友请求列表""" + group: list[GroupRequestResult] = [] + """群组请求列表""" + + +class UserDetail(BaseModel): + """ + 用户详情 + """ + + user_id: str + """用户id""" + ava_url: str + """头像url""" + nickname: str + """昵称""" + remark: str + """备注""" + is_ban: bool + """是否被ban""" + chat_count: int + """发言次数""" + call_count: int + """功能调用次数""" + like_plugin: dict[str, int] + """最喜爱的功能""" + + +class GroupDetail(BaseModel): + """ + 用户详情 + """ + + group_id: str + """群组id""" + ava_url: str + """头像url""" + name: str + """名称""" + member_count: int + """成员数""" + max_member_count: int + """最大成员数""" + chat_count: int + """发言次数""" + call_count: int + """功能调用次数""" + like_plugin: dict[str, int] + """最喜爱的功能""" + level: int + """群权限""" + status: bool + """状态(睡眠)""" + close_plugins: list[Plugin] + """关闭的插件""" + task: list[Task] + """被动列表""" + + +class MessageItem(BaseModel): + + type: str + """消息类型""" + msg: str + """内容""" + + +class Message(BaseModel): + """ + 消息 + """ + + object_id: str + """主体id user_id 或 group_id""" + user_id: str + """用户id""" + group_id: str | None = None + """群组id""" + message: list[MessageItem] + """消息""" + name: str + """用户名称""" + ava_url: str + """用户头像""" + + +class SendMessage(BaseModel): + """ + 发送消息 + """ + + bot_id: str + """bot id""" + user_id: str | None = None + """用户id""" + group_id: str | None = None + """群组id""" + message: str + """消息""" diff --git a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py new file mode 100644 index 00000000..fce8f8e0 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py @@ -0,0 +1,187 @@ +import re + +import cattrs +from fastapi import APIRouter, Query + +from zhenxun.configs.config import Config +from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import BlockType, PluginType + +from ....base_model import Result +from ....utils import authentication +from .model import ( + PluginConfig, + PluginCount, + PluginDetail, + PluginInfo, + PluginSwitch, + UpdatePlugin, +) + +router = APIRouter(prefix="/plugin") + + +@router.get( + "/get_plugin_list", dependencies=[authentication()], deprecated="获取插件列表" # type: ignore +) +async def _( + plugin_type: list[PluginType] = Query(None), menu_type: str | None = None +) -> Result: + try: + plugin_list: list[PluginInfo] = [] + query = DbPluginInfo + if plugin_type: + query = query.filter(plugin_type__in=plugin_type) + if menu_type: + query = query.filter(menu_type=menu_type) + plugins = await query.all() + for plugin in plugins: + plugin_info = PluginInfo( + module=plugin.module, + plugin_name=plugin.name, + default_status=plugin.default_status, + limit_superuser=plugin.limit_superuser, + cost_gold=plugin.cost_gold, + menu_type=plugin.menu_type, + version=plugin.version or 0, + level=plugin.level, + status=plugin.status, + author=plugin.author, + ) + plugin_list.append(plugin_info) + except Exception as e: + logger.error("调用API错误", "/get_plugins", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(plugin_list, "拿到了新鲜出炉的数据!") + + +@router.get( + "/get_plugin_count", dependencies=[authentication()], deprecated="获取插件数量" # type: ignore +) +async def _() -> Result: + plugin_count = PluginCount() + plugin_count.normal = await DbPluginInfo.filter( + plugin_type=PluginType.NORMAL + ).count() + plugin_count.admin = await DbPluginInfo.filter( + plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN] + ).count() + plugin_count.superuser = await DbPluginInfo.filter( + plugin_type=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN] + ).count() + plugin_count.other = await DbPluginInfo.filter( + plugin_type=PluginType.HIDDEN + ).count() + return Result.ok(plugin_count) + + +@router.post( + "/update_plugin", dependencies=[authentication()], description="更新插件参数" +) +async def _(plugin: UpdatePlugin) -> Result: + try: + db_plugin = await DbPluginInfo.get_or_none(module=plugin.module) + if not db_plugin: + return Result.fail("插件不存在...") + db_plugin.default_status = plugin.default_status + db_plugin.limit_superuser = plugin.limit_superuser + db_plugin.cost_gold = plugin.cost_gold + db_plugin.level = plugin.level + db_plugin.menu_type = plugin.menu_type + db_plugin.block_type = plugin.block_type + if plugin.block_type == BlockType.ALL: + db_plugin.status = False + else: + db_plugin.status = True + await db_plugin.save() + # 配置项 + if plugin.configs and (configs := Config.get(plugin.module)): + for key in plugin.configs: + if c := configs.configs.get(key): + value = plugin.configs[key] + if c.type and value is not None: + value = cattrs.structure(value, c.type) + Config.set_config(plugin.module, key, value) + except Exception as e: + logger.error("调用API错误", "/update_plugins", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(info="已经帮你写好啦!") + + +@router.post("/change_switch", dependencies=[authentication()], description="开关插件") +async def _(param: PluginSwitch) -> Result: + db_plugin = await DbPluginInfo.get_or_none(module=param.module) + if not db_plugin: + return Result.fail("插件不存在...") + if not param.status: + db_plugin.block_type = BlockType.ALL + db_plugin.status = False + else: + db_plugin.block_type = None + db_plugin.status = True + await db_plugin.save() + return Result.ok(info="成功改变了开关状态!") + + +@router.get( + "/get_plugin_menu_type", dependencies=[authentication()], description="获取插件类型" +) +async def _() -> Result: + menu_type_list = [] + result = await DbPluginInfo.annotate().values_list("menu_type", flat=True) + for r in result: + if r not in menu_type_list: + menu_type_list.append(r) + return Result.ok(menu_type_list) + + +@router.get("/get_plugin", dependencies=[authentication()], description="获取插件详情") +async def _(module: str) -> Result: + db_plugin = await DbPluginInfo.get_or_none(module=module) + if not db_plugin: + return Result.fail("插件不存在...") + config_list = [] + if config := Config.get(module): + for cfg in config.configs: + type_str = "" + type_inner = None + x = str(config.configs[cfg].type) + r = re.search(r"", str(config.configs[cfg].type)) + if r: + type_str = r.group(1) + else: + r = re.search(r"typing\.(.*)\[(.*)\]", str(config.configs[cfg].type)) + if r: + type_str = r.group(1) + if type_str: + type_str = type_str.lower() + type_inner = r.group(2) + if type_inner: + type_inner = [x.strip() for x in type_inner.split(",")] + config_list.append( + PluginConfig( + module=module, + key=cfg, + value=config.configs[cfg].value, + help=config.configs[cfg].help, + default_value=config.configs[cfg].default_value, + type=type_str, + type_inner=type_inner, # type: ignore + ) + ) + plugin_info = PluginDetail( + module=module, + plugin_name=db_plugin.name, + default_status=db_plugin.default_status, + limit_superuser=db_plugin.limit_superuser, + cost_gold=db_plugin.cost_gold, + menu_type=db_plugin.menu_type, + version=db_plugin.version or "0", + level=db_plugin.level, + status=db_plugin.status, + author=db_plugin.author, + config_list=config_list, + block_type=db_plugin.block_type, + ) + return Result.ok(plugin_info) diff --git a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py new file mode 100644 index 00000000..e2952038 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py @@ -0,0 +1,125 @@ +from typing import Any + +from pydantic import BaseModel + +from zhenxun.utils.enum import BlockType + + +class PluginSwitch(BaseModel): + """ + 插件开关 + """ + + module: str + """模块""" + status: bool + """开关状态""" + + +class UpdateConfig(BaseModel): + """ + 配置项修改参数 + """ + + module: str + """模块""" + key: str + """配置项key""" + value: Any + """配置项值""" + + +class UpdatePlugin(BaseModel): + """ + 插件修改参数 + """ + + module: str + """模块""" + default_status: bool + """默认开关""" + limit_superuser: bool + """限制超级用户""" + cost_gold: int + """金币花费""" + menu_type: str + """插件菜单类型""" + level: int + """插件所需群权限""" + block_type: BlockType | None = None + """禁用类型""" + configs: dict[str, Any] | None = None + """配置项""" + + +class PluginInfo(BaseModel): + """ + 基本插件信息 + """ + + module: str + """插件名称""" + plugin_name: str + """插件中文名称""" + default_status: bool + """默认开关""" + limit_superuser: bool + """限制超级用户""" + cost_gold: int + """花费金币""" + menu_type: str + """插件菜单类型""" + version: str + """插件版本""" + level: int + """群权限""" + status: bool + """当前状态""" + author: str | None = None + """作者""" + block_type: BlockType | None = None + """禁用类型""" + + +class PluginConfig(BaseModel): + """ + 插件配置项 + """ + + module: str + """模块""" + key: str + """键""" + value: Any + """值""" + help: str | None = None + """帮助""" + default_value: Any + """默认值""" + type: Any = None + """值类型""" + type_inner: list[str] | None = None + """List Tuple等内部类型检验""" + + +class PluginCount(BaseModel): + """ + 插件数量 + """ + + normal: int = 0 + """普通插件""" + admin: int = 0 + """管理员插件""" + superuser: int = 0 + """超级用户插件""" + other: int = 0 + """其他插件""" + + +class PluginDetail(PluginInfo): + """ + 插件详情 + """ + + config_list: list[PluginConfig] diff --git a/zhenxun/plugins/web_ui/api/tabs/system/__init__.py b/zhenxun/plugins/web_ui/api/tabs/system/__init__.py new file mode 100644 index 00000000..61430144 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/system/__init__.py @@ -0,0 +1,121 @@ +import os +import shutil +from pathlib import Path +from typing import List, Optional + +from fastapi import APIRouter + +from ....base_model import Result +from ....utils import authentication, get_system_disk +from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile + +router = APIRouter(prefix="/system") + + + +@router.get("/get_dir_list", dependencies=[authentication()], description="获取文件列表") +async def _(path: Optional[str] = None) -> Result: + base_path = Path(path) if path else Path() + data_list = [] + for file in os.listdir(base_path): + data_list.append(DirFile(is_file=not (base_path / file).is_dir(), name=file, parent=path)) + return Result.ok(data_list) + + +@router.get("/get_resources_size", dependencies=[authentication()], description="获取文件列表") +async def _(full_path: Optional[str] = None) -> Result: + return Result.ok(await get_system_disk(full_path)) + + +@router.post("/delete_file", dependencies=[authentication()], description="删除文件") +async def _(param: DeleteFile) -> Result: + path = Path(param.full_path) + if not path or not path.exists(): + return Result.warning_("文件不存在...") + try: + path.unlink() + return Result.ok('删除成功!') + except Exception as e: + return Result.warning_('删除失败: ' + str(e)) + +@router.post("/delete_folder", dependencies=[authentication()], description="删除文件夹") +async def _(param: DeleteFile) -> Result: + path = Path(param.full_path) + if not path or not path.exists() or path.is_file(): + return Result.warning_("文件夹不存在...") + try: + shutil.rmtree(path.absolute()) + return Result.ok('删除成功!') + except Exception as e: + return Result.warning_('删除失败: ' + str(e)) + + +@router.post("/rename_file", dependencies=[authentication()], description="重命名文件") +async def _(param: RenameFile) -> Result: + path = (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) + if not path or not path.exists(): + return Result.warning_("文件不存在...") + try: + path.rename(path.parent / param.name) + return Result.ok('重命名成功!') + except Exception as e: + return Result.warning_('重命名失败: ' + str(e)) + + +@router.post("/rename_folder", dependencies=[authentication()], description="重命名文件夹") +async def _(param: RenameFile) -> Result: + path = (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) + if not path or not path.exists() or path.is_file(): + return Result.warning_("文件夹不存在...") + try: + new_path = path.parent / param.name + shutil.move(path.absolute(), new_path.absolute()) + return Result.ok('重命名成功!') + except Exception as e: + return Result.warning_('重命名失败: ' + str(e)) + + +@router.post("/add_file", dependencies=[authentication()], description="新建文件") +async def _(param: AddFile) -> Result: + path = (Path(param.parent) / param.name) if param.parent else Path(param.name) + if path.exists(): + return Result.warning_("文件已存在...") + try: + path.open('w') + return Result.ok('新建文件成功!') + except Exception as e: + return Result.warning_('新建文件失败: ' + str(e)) + + +@router.post("/add_folder", dependencies=[authentication()], description="新建文件夹") +async def _(param: AddFile) -> Result: + path = (Path(param.parent) / param.name) if param.parent else Path(param.name) + if path.exists(): + return Result.warning_("文件夹已存在...") + try: + path.mkdir() + return Result.ok('新建文件夹成功!') + except Exception as e: + return Result.warning_('新建文件夹失败: ' + str(e)) + + +@router.get("/read_file", dependencies=[authentication()], description="读取文件") +async def _(full_path: str) -> Result: + path = Path(full_path) + if not path.exists(): + return Result.warning_("文件不存在...") + try: + text = path.read_text(encoding='utf-8') + return Result.ok(text) + except Exception as e: + return Result.warning_('新建文件夹失败: ' + str(e)) + +@router.post("/save_file", dependencies=[authentication()], description="读取文件") +async def _(param: SaveFile) -> Result: + path = Path(param.full_path) + try: + with path.open('w') as f: + f.write(param.content) + return Result.ok("更新成功!") + except Exception as e: + return Result.warning_('新建文件夹失败: ' + str(e)) \ No newline at end of file diff --git a/zhenxun/plugins/web_ui/api/tabs/system/model.py b/zhenxun/plugins/web_ui/api/tabs/system/model.py new file mode 100644 index 00000000..b3b5a45f --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/system/model.py @@ -0,0 +1,64 @@ + + + +from datetime import datetime +from typing import Literal, Optional + +from pydantic import BaseModel + + +class DirFile(BaseModel): + + """ + 文件或文件夹 + """ + + is_file: bool + """是否为文件""" + name: str + """文件夹或文件名称""" + parent: Optional[str] = None + """父级""" + +class DeleteFile(BaseModel): + + """ + 删除文件 + """ + + full_path: str + """文件全路径""" + +class RenameFile(BaseModel): + + """ + 删除文件 + """ + parent: Optional[str] + """父路径""" + old_name: str + """旧名称""" + name: str + """新名称""" + + +class AddFile(BaseModel): + + """ + 新建文件 + """ + parent: Optional[str] + """父路径""" + name: str + """新名称""" + + +class SaveFile(BaseModel): + + """ + 保存文件 + """ + full_path: str + """全路径""" + content: str + """内容""" diff --git a/zhenxun/plugins/web_ui/auth/__init__.py b/zhenxun/plugins/web_ui/auth/__init__.py new file mode 100644 index 00000000..6551d1ad --- /dev/null +++ b/zhenxun/plugins/web_ui/auth/__init__.py @@ -0,0 +1,47 @@ +import json +from datetime import timedelta + +import nonebot +from fastapi import APIRouter, Depends +from fastapi.security import OAuth2PasswordRequestForm + +from zhenxun.configs.config import Config + +from ..base_model import Result +from ..utils import ( + ACCESS_TOKEN_EXPIRE_MINUTES, + create_token, + get_user, + token_data, + token_file, +) + +app = nonebot.get_app() + + +router = APIRouter() + + +@router.post("/login") +async def login_get_token(form_data: OAuth2PasswordRequestForm = Depends()): + username = Config.get_config("web-ui", "username") + password = Config.get_config("web-ui", "password") + if not username or not password: + return Result.fail("你滴配置文件里用户名密码配置项为空", 998) + if username != form_data.username or password != form_data.password: + return Result.fail("真笨, 账号密码都能记错!", 999) + user = get_user(form_data.username) + if not user: + return Result.fail("用户不存在...", 997) + access_token = create_token( + user=user, + expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), + ) + token_data["token"].append(access_token) + if len(token_data["token"]) > 3: + token_data["token"] = token_data["token"][1:] + with open(token_file, "w", encoding="utf8") as f: + json.dump(token_data, f, ensure_ascii=False, indent=4) + return Result.ok( + {"access_token": access_token, "token_type": "bearer"}, "欢迎回家, 欧尼酱!" + ) diff --git a/zhenxun/plugins/web_ui/base_model.py b/zhenxun/plugins/web_ui/base_model.py new file mode 100644 index 00000000..67bb280f --- /dev/null +++ b/zhenxun/plugins/web_ui/base_model.py @@ -0,0 +1,108 @@ +from datetime import datetime +from typing import Any, Generic, Optional, TypeVar + +from pydantic import BaseModel, validator +from typing_extensions import Self + +T = TypeVar("T") + + +class User(BaseModel): + username: str + password: str + + +class Token(BaseModel): + access_token: str + token_type: str + + +class Result(BaseModel): + """ + 总体返回 + """ + + suc: bool + """调用状态""" + code: int = 200 + """code""" + info: str = "操作成功" + """info""" + warning: Optional[str] = None + """警告信息""" + data: Any = None + """返回数据""" + + @classmethod + def warning_(cls, info: str, code: int = 200) -> Self: + return cls(suc=True, warning=info, code=code) + + @classmethod + def fail(cls, info: str = "异常错误", code: int = 500) -> Self: + return cls(suc=False, info=info, code=code) + + @classmethod + def ok(cls, data: Any = None, info: str = "操作成功", code: int = 200) -> Self: + return cls(suc=True, info=info, code=code, data=data) + + +class QueryModel(BaseModel, Generic[T]): + """ + 基本查询条件 + """ + + index: int + """页数""" + size: int + """每页数量""" + data: T + """携带数据""" + + @validator("index") + def index_validator(cls, index): + if index < 1: + raise ValueError("查询下标小于1...") + return index + + @validator("size") + def size_validator(cls, size): + if size < 1: + raise ValueError("每页数量小于1...") + return size + + +class BaseResultModel(BaseModel): + """ + 基础返回 + """ + + total: int + """总页数""" + data: Any + """数据""" + + +class SystemStatus(BaseModel): + """ + 系统状态 + """ + + cpu: float + memory: float + disk: float + check_time: datetime + + +class SystemFolderSize(BaseModel): + """ + 资源文件占比 + """ + + name: str + """名称""" + size: float + """大小""" + full_path: Optional[str] + """完整路径""" + is_dir: bool + """是否为文件夹""" diff --git a/zhenxun/plugins/web_ui/config.py b/zhenxun/plugins/web_ui/config.py new file mode 100644 index 00000000..0f16949a --- /dev/null +++ b/zhenxun/plugins/web_ui/config.py @@ -0,0 +1,36 @@ +import nonebot +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from strenum import StrEnum + +app = nonebot.get_app() + +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +AVA_URL = "http://q1.qlogo.cn/g?b=qq&nk={}&s=160" + +GROUP_AVA_URL = "http://p.qlogo.cn/gh/{}/{}/640/" + + +class QueryDateType(StrEnum): + """ + 查询日期类型 + """ + + DAY = "day" + """日""" + WEEK = "week" + """周""" + MONTH = "month" + """月""" + YEAR = "year" + """年""" diff --git a/zhenxun/plugins/web_ui/utils.py b/zhenxun/plugins/web_ui/utils.py new file mode 100644 index 00000000..f39f36ac --- /dev/null +++ b/zhenxun/plugins/web_ui/utils.py @@ -0,0 +1,136 @@ +import os +from datetime import datetime, timedelta +from pathlib import Path + +import psutil +import ujson as json +from fastapi import Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from nonebot.utils import run_sync + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH + +from .base_model import SystemFolderSize, SystemStatus, User + +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/login") + +token_file = DATA_PATH / "web_ui" / "token.json" +token_file.parent.mkdir(parents=True, exist_ok=True) +token_data = {"token": []} +if token_file.exists(): + try: + token_data = json.load(open(token_file, "r", encoding="utf8")) + except json.JSONDecodeError: + pass + + +def get_user(uname: str) -> User | None: + """获取账号密码 + + 参数: + uname: uname + + 返回: + Optional[User]: 用户信息 + """ + username = Config.get_config("web-ui", "username") + password = Config.get_config("web-ui", "password") + if username and password and uname == username: + return User(username=username, password=password) + + +def create_token(user: User, expires_delta: timedelta | None = None): + """创建token + + 参数: + user: 用户信息 + expires_delta: 过期时间. + """ + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15)) + return jwt.encode( + claims={"sub": user.username, "exp": expire}, + key=SECRET_KEY, + algorithm=ALGORITHM, + ) + + +def authentication(): + """权限验证 + + 异常: + JWTError: JWTError + HTTPException: HTTPException + """ + + # if token not in token_data["token"]: + def inner(token: str = Depends(oauth2_scheme)): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username, expire = payload.get("sub"), payload.get("exp") + user = get_user(username) # type: ignore + if user is None: + raise JWTError + except JWTError: + raise HTTPException( + status_code=400, detail="登录验证失败或已失效, 踢出房间!" + ) + + return Depends(inner) + + +def _get_dir_size(dir_path: Path) -> float: + """获取文件夹大小 + + 参数: + dir_path: 文件夹路径 + """ + size = 0 + for root, dirs, files in os.walk(dir_path): + size += sum([os.path.getsize(os.path.join(root, name)) for name in files]) + return size + + +@run_sync +def get_system_status() -> SystemStatus: + """获取系统信息等""" + cpu = psutil.cpu_percent() + memory = psutil.virtual_memory().percent + disk = psutil.disk_usage("/").percent + return SystemStatus( + cpu=cpu, + memory=memory, + disk=disk, + check_time=datetime.now().replace(microsecond=0), + ) + + +@run_sync +def get_system_disk( + full_path: str | None, +) -> list[SystemFolderSize]: + """获取资源文件大小等""" + base_path = Path(full_path) if full_path else Path() + other_size = 0 + data_list = [] + for file in os.listdir(base_path): + f = base_path / file + if f.is_dir(): + size = _get_dir_size(f) / 1024 / 1024 + data_list.append( + SystemFolderSize(name=file, size=size, full_path=str(f), is_dir=True) + ) + else: + other_size += f.stat().st_size / 1024 / 1024 + if other_size: + data_list.append( + SystemFolderSize( + name="other_file", size=other_size, full_path=full_path, is_dir=False + ) + ) + return data_list diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py index 681f9768..bf6c5daa 100644 --- a/zhenxun/utils/enum.py +++ b/zhenxun/utils/enum.py @@ -97,3 +97,5 @@ class RequestHandleType(StrEnum): """拒绝""" IGNORE = "IGNORE" """忽略""" + EXPIRE = "EXPIRE" + """过期或失效""" diff --git a/zhenxun/utils/plugin_models/base.py b/zhenxun/utils/plugin_models/base.py new file mode 100644 index 00000000..a50f6b4a --- /dev/null +++ b/zhenxun/utils/plugin_models/base.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class CommonSql(BaseModel): + + sql: str + """sql语句""" + remark: str + """备注"""