mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-15 06:12:53 +08:00
feat✨: 新增Web UI功能及数据库、日志等API接口
This commit is contained in:
parent
f0b05ec5ed
commit
2bf5fd1a37
210
poetry.lock
generated
210
poetry.lock
generated
@ -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"
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -168,6 +168,8 @@ class PluginExtraData(BaseModel):
|
||||
"""超级用户帮助"""
|
||||
aliases: Set[str] = set()
|
||||
"""额外名称"""
|
||||
sql_list: list[str] | None = None
|
||||
"""常用sql"""
|
||||
|
||||
|
||||
class NoSuchConfig(Exception):
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
76
zhenxun/plugins/web_ui/__init__.py
Normal file
76
zhenxun/plugins/web_ui/__init__.py
Normal file
@ -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("<g>API启动成功</g>", "Web UI")
|
||||
except Exception as e:
|
||||
logger.error("<g>API启动失败</g>", "Web UI", e=e)
|
||||
1
zhenxun/plugins/web_ui/api/__init__.py
Normal file
1
zhenxun/plugins/web_ui/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .tabs import *
|
||||
1
zhenxun/plugins/web_ui/api/logs/__init__.py
Normal file
1
zhenxun/plugins/web_ui/api/logs/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .logs import *
|
||||
35
zhenxun/plugins/web_ui/api/logs/log_manager.py
Normal file
35
zhenxun/plugins/web_ui/api/logs/log_manager.py
Normal file
@ -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]()
|
||||
40
zhenxun/plugins/web_ui/api/logs/logs.py
Normal file
40
zhenxun/plugins/web_ui/api/logs/logs.py
Normal file
@ -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"<e>{escape_tag(repr(recv))}</e>"
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
LOG_STORAGE.listeners.remove(log_listener)
|
||||
return
|
||||
5
zhenxun/plugins/web_ui/api/tabs/__init__.py
Normal file
5
zhenxun/plugins/web_ui/api/tabs/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .database import *
|
||||
from .main import *
|
||||
from .manage import *
|
||||
from .plugin_manage import *
|
||||
from .system import *
|
||||
121
zhenxun/plugins/web_ui/api/tabs/database/__init__.py
Normal file
121
zhenxun/plugins/web_ui/api/tabs/database/__init__.py
Normal file
@ -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))
|
||||
24
zhenxun/plugins/web_ui/api/tabs/database/models/model.py
Normal file
24
zhenxun/plugins/web_ui/api/tabs/database/models/model.py
Normal file
@ -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]
|
||||
"""插件列表"""
|
||||
37
zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py
Normal file
37
zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py
Normal file
@ -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)
|
||||
290
zhenxun/plugins/web_ui/api/tabs/main/__init__.py
Normal file
290
zhenxun/plugins/web_ui/api/tabs/main/__init__.py
Normal file
@ -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
|
||||
35
zhenxun/plugins/web_ui/api/tabs/main/data_source.py
Normal file
35
zhenxun/plugins/web_ui/api/tabs/main/data_source.py
Normal file
@ -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)
|
||||
105
zhenxun/plugins/web_ui/api/tabs/main/model.py
Normal file
105
zhenxun/plugins/web_ui/api/tabs/main/model.py
Normal file
@ -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
|
||||
"""调用次数"""
|
||||
529
zhenxun/plugins/web_ui/api/tabs/manage/__init__.py
Normal file
529
zhenxun/plugins/web_ui/api/tabs/manage/__init__.py
Normal file
@ -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
|
||||
265
zhenxun/plugins/web_ui/api/tabs/manage/model.py
Normal file
265
zhenxun/plugins/web_ui/api/tabs/manage/model.py
Normal file
@ -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
|
||||
"""消息"""
|
||||
187
zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py
Normal file
187
zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py
Normal file
@ -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"<class '(.*)'>", 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)
|
||||
125
zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py
Normal file
125
zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py
Normal file
@ -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]
|
||||
121
zhenxun/plugins/web_ui/api/tabs/system/__init__.py
Normal file
121
zhenxun/plugins/web_ui/api/tabs/system/__init__.py
Normal file
@ -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))
|
||||
64
zhenxun/plugins/web_ui/api/tabs/system/model.py
Normal file
64
zhenxun/plugins/web_ui/api/tabs/system/model.py
Normal file
@ -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
|
||||
"""内容"""
|
||||
47
zhenxun/plugins/web_ui/auth/__init__.py
Normal file
47
zhenxun/plugins/web_ui/auth/__init__.py
Normal file
@ -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"}, "欢迎回家, 欧尼酱!"
|
||||
)
|
||||
108
zhenxun/plugins/web_ui/base_model.py
Normal file
108
zhenxun/plugins/web_ui/base_model.py
Normal file
@ -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
|
||||
"""是否为文件夹"""
|
||||
36
zhenxun/plugins/web_ui/config.py
Normal file
36
zhenxun/plugins/web_ui/config.py
Normal file
@ -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"
|
||||
"""年"""
|
||||
136
zhenxun/plugins/web_ui/utils.py
Normal file
136
zhenxun/plugins/web_ui/utils.py
Normal file
@ -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
|
||||
@ -97,3 +97,5 @@ class RequestHandleType(StrEnum):
|
||||
"""拒绝"""
|
||||
IGNORE = "IGNORE"
|
||||
"""忽略"""
|
||||
EXPIRE = "EXPIRE"
|
||||
"""过期或失效"""
|
||||
|
||||
9
zhenxun/utils/plugin_models/base.py
Normal file
9
zhenxun/utils/plugin_models/base.py
Normal file
@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CommonSql(BaseModel):
|
||||
|
||||
sql: str
|
||||
"""sql语句"""
|
||||
remark: str
|
||||
"""备注"""
|
||||
Loading…
Reference in New Issue
Block a user