entari-plugin-webui 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- entari_plugin_webui/__init__.py +131 -0
- entari_plugin_webui/adapter.py +232 -0
- entari_plugin_webui/api/__init__.py +3 -0
- entari_plugin_webui/api/auth.py +107 -0
- entari_plugin_webui/api/config.py +39 -0
- entari_plugin_webui/api/deps.py +24 -0
- entari_plugin_webui/api/extensions.py +21 -0
- entari_plugin_webui/api/health.py +21 -0
- entari_plugin_webui/api/logs.py +40 -0
- entari_plugin_webui/api/market.py +63 -0
- entari_plugin_webui/api/menus.py +29 -0
- entari_plugin_webui/api/plugins.py +51 -0
- entari_plugin_webui/api/router.py +101 -0
- entari_plugin_webui/api/stats.py +13 -0
- entari_plugin_webui/config.py +16 -0
- entari_plugin_webui/core/__init__.py +1 -0
- entari_plugin_webui/core/audit.py +8 -0
- entari_plugin_webui/core/error.py +58 -0
- entari_plugin_webui/core/extension.py +134 -0
- entari_plugin_webui/core/i18n.py +21 -0
- entari_plugin_webui/core/log_stream.py +71 -0
- entari_plugin_webui/core/permissions.py +17 -0
- entari_plugin_webui/core/security.py +89 -0
- entari_plugin_webui/core/session.py +55 -0
- entari_plugin_webui/models/__init__.py +1 -0
- entari_plugin_webui/models/stats.py +13 -0
- entari_plugin_webui/services/__init__.py +1 -0
- entari_plugin_webui/services/config_service.py +180 -0
- entari_plugin_webui/services/market_service.py +181 -0
- entari_plugin_webui/services/package_manager.py +113 -0
- entari_plugin_webui/services/plugin_service.py +96 -0
- entari_plugin_webui/services/stats_service.py +80 -0
- entari_plugin_webui/static/.gitkeep +0 -0
- entari_plugin_webui/static/frontend/assets/Chat-CaGJsFaQ.css +1 -0
- entari_plugin_webui/static/frontend/assets/Chat-CtSM30GL.js +3 -0
- entari_plugin_webui/static/frontend/assets/Dashboard-DWOzkbJ2.css +1 -0
- entari_plugin_webui/static/frontend/assets/Dashboard-VhlTN5Pp.js +1 -0
- entari_plugin_webui/static/frontend/assets/ExtensionPage-CqCQibPd.js +1 -0
- entari_plugin_webui/static/frontend/assets/ExtensionPage-jLULI21o.css +1 -0
- entari_plugin_webui/static/frontend/assets/Login-DWplnA3S.js +1 -0
- entari_plugin_webui/static/frontend/assets/Login-Z9pRTEPM.css +1 -0
- entari_plugin_webui/static/frontend/assets/Logs-ZKt_DKBY.js +3 -0
- entari_plugin_webui/static/frontend/assets/Logs-woONf-h0.css +1 -0
- entari_plugin_webui/static/frontend/assets/Market-CBFEZqhH.css +1 -0
- entari_plugin_webui/static/frontend/assets/Market-DRJ1QS_i.js +1 -0
- entari_plugin_webui/static/frontend/assets/Settings-BMb0Nxa4.js +944 -0
- entari_plugin_webui/static/frontend/assets/Settings-DZ6dtk9x.css +1 -0
- entari_plugin_webui/static/frontend/assets/abap-DRC6TkPh.js +6 -0
- entari_plugin_webui/static/frontend/assets/apex-BuapDI9Y.js +6 -0
- entari_plugin_webui/static/frontend/assets/azcli-BypH-vXm.js +6 -0
- entari_plugin_webui/static/frontend/assets/bat-BY6pwuIY.js +6 -0
- entari_plugin_webui/static/frontend/assets/bicep-gRuQeaLk.js +7 -0
- entari_plugin_webui/static/frontend/assets/c-BIGW1oBm.js +1 -0
- entari_plugin_webui/static/frontend/assets/cameligo-ul-Lp4lw.js +6 -0
- entari_plugin_webui/static/frontend/assets/clojure-DeYg-96x.js +6 -0
- entari_plugin_webui/static/frontend/assets/codicon-DCmgc-ay.ttf +0 -0
- entari_plugin_webui/static/frontend/assets/coffee-CfnpWUYo.js +6 -0
- entari_plugin_webui/static/frontend/assets/cpp-C9L3yaDO.js +6 -0
- entari_plugin_webui/static/frontend/assets/cpp-DIPi6g--.js +1 -0
- entari_plugin_webui/static/frontend/assets/csharp-DWGz5Zuj.js +6 -0
- entari_plugin_webui/static/frontend/assets/csp-DrRCxMg5.js +6 -0
- entari_plugin_webui/static/frontend/assets/css-BfLuTCmN.js +8 -0
- entari_plugin_webui/static/frontend/assets/css-CLj8gQPS.js +1 -0
- entari_plugin_webui/static/frontend/assets/cssMode-Bb6LZ5kP.js +9 -0
- entari_plugin_webui/static/frontend/assets/cypher-DoFvH58O.js +6 -0
- entari_plugin_webui/static/frontend/assets/dart-DIovg4uR.js +6 -0
- entari_plugin_webui/static/frontend/assets/docker-BcOcwvcX.js +1 -0
- entari_plugin_webui/static/frontend/assets/dockerfile-D2PfwrvU.js +6 -0
- entari_plugin_webui/static/frontend/assets/ecl-C_scCXcs.js +6 -0
- entari_plugin_webui/static/frontend/assets/editor.worker-Z-F9bRfX.js +11 -0
- entari_plugin_webui/static/frontend/assets/elixir-BRk-K-rg.js +6 -0
- entari_plugin_webui/static/frontend/assets/flow9-DLs3tTet.js +6 -0
- entari_plugin_webui/static/frontend/assets/freemarker2-C6tiMqiu.js +8 -0
- entari_plugin_webui/static/frontend/assets/fsharp-D0UiDa5C.js +6 -0
- entari_plugin_webui/static/frontend/assets/go-C27-OAKa.js +1 -0
- entari_plugin_webui/static/frontend/assets/go-CyVeKkvQ.js +6 -0
- entari_plugin_webui/static/frontend/assets/graphql-BygKL3ZF.js +6 -0
- entari_plugin_webui/static/frontend/assets/handlebars-BaFdx4N6.js +6 -0
- entari_plugin_webui/static/frontend/assets/hcl-D_OY6ada.js +6 -0
- entari_plugin_webui/static/frontend/assets/html-kcteW3br.js +6 -0
- entari_plugin_webui/static/frontend/assets/html-pp8916En.js +1 -0
- entari_plugin_webui/static/frontend/assets/htmlMode-DX-hmsgL.js +9 -0
- entari_plugin_webui/static/frontend/assets/index-CNTptdPF.js +178 -0
- entari_plugin_webui/static/frontend/assets/index-DGqFI5c2.css +1 -0
- entari_plugin_webui/static/frontend/assets/ini-BEwlwnbL.js +1 -0
- entari_plugin_webui/static/frontend/assets/ini-BTpWsGps.js +6 -0
- entari_plugin_webui/static/frontend/assets/java-3TATJI7h.js +6 -0
- entari_plugin_webui/static/frontend/assets/java-CylS5w8V.js +1 -0
- entari_plugin_webui/static/frontend/assets/javascript-BmsOxqGW.js +6 -0
- entari_plugin_webui/static/frontend/assets/javascript-wDzz0qaB.js +1 -0
- entari_plugin_webui/static/frontend/assets/json-Cp-IABpG.js +1 -0
- entari_plugin_webui/static/frontend/assets/json.worker-aYIWSs1k.js +48 -0
- entari_plugin_webui/static/frontend/assets/jsonMode-DKWQYMua.js +15 -0
- entari_plugin_webui/static/frontend/assets/jsx-g9-lgVsj.js +1 -0
- entari_plugin_webui/static/frontend/assets/julia-DDpSJMW6.js +6 -0
- entari_plugin_webui/static/frontend/assets/kotlin-DVYH6Lj_.js +6 -0
- entari_plugin_webui/static/frontend/assets/less-CuFlys0T.js +7 -0
- entari_plugin_webui/static/frontend/assets/lexon-m09vb5r-.js +6 -0
- entari_plugin_webui/static/frontend/assets/liquid-u0eWJ0Zm.js +6 -0
- entari_plugin_webui/static/frontend/assets/logo-BLRhia6f.svg +71 -0
- entari_plugin_webui/static/frontend/assets/lua-D2Z7JJdl.js +6 -0
- entari_plugin_webui/static/frontend/assets/m3-B2Cf9XSq.js +6 -0
- entari_plugin_webui/static/frontend/assets/markdown-BXYnMxBe.js +6 -0
- entari_plugin_webui/static/frontend/assets/markdown-Cvjx9yec.js +1 -0
- entari_plugin_webui/static/frontend/assets/mdx-2iLb9JPW.js +6 -0
- entari_plugin_webui/static/frontend/assets/mips-Ckkbw-AO.js +6 -0
- entari_plugin_webui/static/frontend/assets/msdax-B5uW3Zvf.js +6 -0
- entari_plugin_webui/static/frontend/assets/mysql-B8ssZoUh.js +6 -0
- entari_plugin_webui/static/frontend/assets/objective-c-CrrKwR0a.js +6 -0
- entari_plugin_webui/static/frontend/assets/pascal-BWBTHuhh.js +6 -0
- entari_plugin_webui/static/frontend/assets/pascaligo-BGLI1Hdo.js +6 -0
- entari_plugin_webui/static/frontend/assets/perl-DDrv2Hr-.js +6 -0
- entari_plugin_webui/static/frontend/assets/pgsql-DLPipH_Q.js +6 -0
- entari_plugin_webui/static/frontend/assets/php-CTNlIIiR.js +6 -0
- entari_plugin_webui/static/frontend/assets/pla-2oJWbEOo.js +6 -0
- entari_plugin_webui/static/frontend/assets/postiats-DOk3G3cc.js +6 -0
- entari_plugin_webui/static/frontend/assets/powerquery-Dgyr3wWZ.js +6 -0
- entari_plugin_webui/static/frontend/assets/powershell-B_i9asfM.js +6 -0
- entari_plugin_webui/static/frontend/assets/powershell-Dpen1YoG.js +1 -0
- entari_plugin_webui/static/frontend/assets/protobuf-CV9EbfTh.js +7 -0
- entari_plugin_webui/static/frontend/assets/pug-CCBS_C5_.js +6 -0
- entari_plugin_webui/static/frontend/assets/python-B6aJPvgy.js +1 -0
- entari_plugin_webui/static/frontend/assets/python-DRt4FMw4.js +6 -0
- entari_plugin_webui/static/frontend/assets/qsharp-BLuZWbUW.js +6 -0
- entari_plugin_webui/static/frontend/assets/r-CzF1MCbP.js +6 -0
- entari_plugin_webui/static/frontend/assets/razor-BhGEOgFX.js +6 -0
- entari_plugin_webui/static/frontend/assets/redis-C75U4IDy.js +6 -0
- entari_plugin_webui/static/frontend/assets/redshift-Bc5xkKR1.js +6 -0
- entari_plugin_webui/static/frontend/assets/restructuredtext-DmdQbaLT.js +6 -0
- entari_plugin_webui/static/frontend/assets/ruby-DB0RB20n.js +6 -0
- entari_plugin_webui/static/frontend/assets/rust-B1yitclQ.js +1 -0
- entari_plugin_webui/static/frontend/assets/rust-UMmp-gVE.js +6 -0
- entari_plugin_webui/static/frontend/assets/sb-DVG02705.js +6 -0
- entari_plugin_webui/static/frontend/assets/scala-DvSxYeG4.js +6 -0
- entari_plugin_webui/static/frontend/assets/scheme-yf5bffbF.js +6 -0
- entari_plugin_webui/static/frontend/assets/scss-Bzb7OGdO.js +8 -0
- entari_plugin_webui/static/frontend/assets/shell-CEILq0vU.js +1 -0
- entari_plugin_webui/static/frontend/assets/shell-FNqbgIOG.js +6 -0
- entari_plugin_webui/static/frontend/assets/solidity-DyKutqhl.js +6 -0
- entari_plugin_webui/static/frontend/assets/sophia-B4VqtPa2.js +6 -0
- entari_plugin_webui/static/frontend/assets/sparql-B7alP455.js +6 -0
- entari_plugin_webui/static/frontend/assets/sql-CRqJ_cUM.js +1 -0
- entari_plugin_webui/static/frontend/assets/sql-D7lU1fdU.js +6 -0
- entari_plugin_webui/static/frontend/assets/st-VuadG5SK.js +6 -0
- entari_plugin_webui/static/frontend/assets/swift-BYtUz8ZP.js +8 -0
- entari_plugin_webui/static/frontend/assets/systemverilog-DOAuugfS.js +6 -0
- entari_plugin_webui/static/frontend/assets/tcl-CXKOl_mN.js +6 -0
- entari_plugin_webui/static/frontend/assets/toml-vGWfd6FD.js +1 -0
- entari_plugin_webui/static/frontend/assets/tsMode-D_fzWOEs.js +16 -0
- entari_plugin_webui/static/frontend/assets/tsx-COt5Ahok.js +1 -0
- entari_plugin_webui/static/frontend/assets/twig-D9yiNO04.js +6 -0
- entari_plugin_webui/static/frontend/assets/typescript-BPQ3VLAy.js +1 -0
- entari_plugin_webui/static/frontend/assets/typescript-C809Del3.js +6 -0
- entari_plugin_webui/static/frontend/assets/typespec-BupSXVCO.js +6 -0
- entari_plugin_webui/static/frontend/assets/vb-ZlaFEk-P.js +6 -0
- entari_plugin_webui/static/frontend/assets/vue-DMJtu8ND.js +1 -0
- entari_plugin_webui/static/frontend/assets/wgsl-B-lZjTdr.js +303 -0
- entari_plugin_webui/static/frontend/assets/xml-CRM7UJ29.js +6 -0
- entari_plugin_webui/static/frontend/assets/xml-sdJ4AIDG.js +1 -0
- entari_plugin_webui/static/frontend/assets/yaml-Buea-lGh.js +1 -0
- entari_plugin_webui/static/frontend/assets/yaml-C3lZ9fhp.js +6 -0
- entari_plugin_webui/static/frontend/favicon.ico +0 -0
- entari_plugin_webui/static/frontend/favicon.svg +71 -0
- entari_plugin_webui/static/frontend/index.html +3 -0
- entari_plugin_webui/static/marketplace.json +74 -0
- entari_plugin_webui/utils.py +3 -0
- entari_plugin_webui-1.0.0.dist-info/METADATA +131 -0
- entari_plugin_webui-1.0.0.dist-info/RECORD +170 -0
- entari_plugin_webui-1.0.0.dist-info/WHEEL +4 -0
- entari_plugin_webui-1.0.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Entari Plugin: WebUI 服务(重设计版)"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from arclet.entari import plugin
|
|
8
|
+
from arclet.entari.event.lifespan import Startup
|
|
9
|
+
from arclet.entari.event.send import SendResponse
|
|
10
|
+
from arclet.entari.plugin import PluginRole, plugin_config
|
|
11
|
+
from entari_plugin_server import add_route, add_websocket_route, replace_asgi, server
|
|
12
|
+
from fastapi import Request
|
|
13
|
+
from fastapi.staticfiles import StaticFiles
|
|
14
|
+
from starlette.responses import FileResponse, Response
|
|
15
|
+
|
|
16
|
+
from .config import Config
|
|
17
|
+
from .core.extension import MenuItem as MenuItem
|
|
18
|
+
from .core.extension import WebUIExtension as WebUIExtension
|
|
19
|
+
from .core.extension import get_all_extension_routes
|
|
20
|
+
from .core.extension import webui_extend as webui_extend
|
|
21
|
+
from .core.security import (
|
|
22
|
+
LoginThrottle,
|
|
23
|
+
generate_random_password,
|
|
24
|
+
hash_password,
|
|
25
|
+
is_local_deployment,
|
|
26
|
+
parse_rate_limit,
|
|
27
|
+
set_local_mode,
|
|
28
|
+
)
|
|
29
|
+
from .core.session import SessionStore
|
|
30
|
+
from .utils import logger
|
|
31
|
+
|
|
32
|
+
__version__ = "1.0.0"
|
|
33
|
+
_STATIC_DIR = Path(__file__).parent / "static"
|
|
34
|
+
_FRONTEND_DIR = _STATIC_DIR / "frontend"
|
|
35
|
+
|
|
36
|
+
webui_config = plugin_config(Config, bind=True)
|
|
37
|
+
|
|
38
|
+
from .adapter import WebUIAdapter
|
|
39
|
+
from .api import create_app as _create_app # noqa: E402
|
|
40
|
+
from .api import logs # noqa: F401
|
|
41
|
+
from .models.stats import MessageStat # noqa: F401
|
|
42
|
+
from .services.stats_service import increment # noqa: PLC0415
|
|
43
|
+
|
|
44
|
+
_session_store: SessionStore = SessionStore(ttl=webui_config.session_ttl)
|
|
45
|
+
_login_throttle = LoginThrottle(*parse_rate_limit(webui_config.login_rate_limit))
|
|
46
|
+
|
|
47
|
+
if not server.path:
|
|
48
|
+
logger.warning("未检测到 Server 插件的 path 配置,WebUI 可能无法正常工作")
|
|
49
|
+
logger.warning("已自动设置 Server 插件的 path 为 /satori")
|
|
50
|
+
server.path = "/satori"
|
|
51
|
+
|
|
52
|
+
server.apply(WebUIAdapter())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
plugin.metadata(
|
|
56
|
+
"WebUI 服务",
|
|
57
|
+
PluginRole.UTILITY,
|
|
58
|
+
[{"name": "RF-Tar-Railt", "email": "rf_tar_railt@qq.com"}],
|
|
59
|
+
__version__,
|
|
60
|
+
description="基于 Vite + Vue 3 + Element Plus 的可视化管理面板",
|
|
61
|
+
urls={"homepage": "https://github.com/ArcletProject/entari-plugin-webui"},
|
|
62
|
+
config=Config,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------- SPA fallback handlers ----------
|
|
67
|
+
async def _root(request: Request) -> Response:
|
|
68
|
+
if not _FRONTEND_DIR.exists():
|
|
69
|
+
return Response(
|
|
70
|
+
content="Frontend not built. Run 'pdm run build-frontend'.",
|
|
71
|
+
status_code=503,
|
|
72
|
+
)
|
|
73
|
+
path = request.url.path.lstrip("/")
|
|
74
|
+
file = _FRONTEND_DIR / (path or "index.html")
|
|
75
|
+
if file.exists():
|
|
76
|
+
return FileResponse(file)
|
|
77
|
+
return Response(status_code=404)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
replace_asgi(app := _create_app())
|
|
81
|
+
|
|
82
|
+
if _FRONTEND_DIR.exists() and (_FRONTEND_DIR / "assets").exists():
|
|
83
|
+
app.mount("/assets", StaticFiles(directory=_FRONTEND_DIR / "assets", html=True))
|
|
84
|
+
|
|
85
|
+
add_route("/", methods=["GET"], include_in_schema=False)(_root)
|
|
86
|
+
add_route("/favicon.ico", methods=["GET"], include_in_schema=False)(_root)
|
|
87
|
+
add_route("/favicon.svg", methods=["GET"], include_in_schema=False)(_root)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------- SendResponse listener (message counting) ----------
|
|
91
|
+
@plugin.listen(SendResponse)
|
|
92
|
+
async def _on_message_sent(event: SendResponse) -> None:
|
|
93
|
+
platform = event.account.platform
|
|
94
|
+
try:
|
|
95
|
+
await increment(platform)
|
|
96
|
+
except Exception: # noqa: BLE001
|
|
97
|
+
logger.debug("消息计数失败")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------- Startup listener ----------
|
|
101
|
+
@plugin.listen(Startup, priority=100)
|
|
102
|
+
async def _on_startup() -> None:
|
|
103
|
+
is_local = is_local_deployment(server.host)
|
|
104
|
+
set_local_mode(is_local)
|
|
105
|
+
if is_local:
|
|
106
|
+
logger.info("本地部署模式,无需认证")
|
|
107
|
+
else:
|
|
108
|
+
logger.info("远程部署模式,需要认证")
|
|
109
|
+
if not webui_config.password:
|
|
110
|
+
raw = generate_random_password(16)
|
|
111
|
+
webui_config.password = hash_password(raw)
|
|
112
|
+
logger.warning("已生成管理员密码:" + raw)
|
|
113
|
+
|
|
114
|
+
import arclet.entari.logger as entari_log
|
|
115
|
+
from loguru import logger as loguru_logger
|
|
116
|
+
|
|
117
|
+
from .core.log_stream import LogWriter, get_log_buffer
|
|
118
|
+
|
|
119
|
+
log_buffer = get_log_buffer()
|
|
120
|
+
loguru_logger.add(
|
|
121
|
+
LogWriter(log_buffer), level=0, diagnose=True, backtrace=True, colorize=True, filter=entari_log.default_filter
|
|
122
|
+
) # type: ignore[call-overload]
|
|
123
|
+
|
|
124
|
+
routes, ws_routes = get_all_extension_routes()
|
|
125
|
+
for r in routes:
|
|
126
|
+
add_route(r.path, methods=r.methods)(r.handler)
|
|
127
|
+
for w in ws_routes:
|
|
128
|
+
add_websocket_route(w.path)(w.handler)
|
|
129
|
+
|
|
130
|
+
host = server.host or "127.0.0.1"
|
|
131
|
+
logger.info(f"管理面板已启动: http://{host}:{server.port}/")
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from secrets import token_hex
|
|
6
|
+
|
|
7
|
+
from launart import Launart, any_completed
|
|
8
|
+
from launart.status import Phase
|
|
9
|
+
from satori import Api, Event, EventType, LoginStatus
|
|
10
|
+
from satori.exception import ActionFailed
|
|
11
|
+
from satori.model import Channel, ChannelType, Login, MessageObject, User
|
|
12
|
+
from satori.parser import Element, parse
|
|
13
|
+
from satori.server import Request
|
|
14
|
+
from satori.server.adapter import Adapter as BaseAdapter
|
|
15
|
+
from satori.server.route import MessageParam
|
|
16
|
+
from satori.utils import decode, encode
|
|
17
|
+
from starlette.responses import JSONResponse, Response
|
|
18
|
+
from starlette.routing import WebSocketRoute
|
|
19
|
+
from starlette.websockets import WebSocket
|
|
20
|
+
|
|
21
|
+
from .core.security import is_local_mode
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def serialize_element(element: Element):
|
|
25
|
+
return {
|
|
26
|
+
"type": element.type,
|
|
27
|
+
"attrs": element.attrs,
|
|
28
|
+
"children": [serialize_element(child) for child in element.children],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def deserialize_element(data: dict) -> Element:
|
|
33
|
+
return Element(
|
|
34
|
+
data["type"],
|
|
35
|
+
data.get("attrs", {}),
|
|
36
|
+
*(deserialize_element(child) for child in data.get("children", [])),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def elements_to_content(elements: list[dict] | None) -> str:
|
|
41
|
+
if not elements:
|
|
42
|
+
return ""
|
|
43
|
+
return "".join(str(deserialize_element(e)) for e in elements)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def message_receive(raw: dict, conn: WebsocketConnection):
|
|
47
|
+
peer_type = raw.get("peer_type", "user")
|
|
48
|
+
peer_id = f"private:{conn.user_id}" if peer_type == "user" else (raw.get("peer_id") or f"channel:{token_hex(8)}")
|
|
49
|
+
message_id = raw["message_id"]
|
|
50
|
+
message_content = raw.get("message_content") or elements_to_content(raw.get("elements"))
|
|
51
|
+
channel = Channel(peer_id, ChannelType.DIRECT if peer_type == "user" else ChannelType.TEXT)
|
|
52
|
+
return Event(
|
|
53
|
+
EventType.MESSAGE_CREATED,
|
|
54
|
+
datetime.now(),
|
|
55
|
+
conn.adapter.logins[conn.session_id],
|
|
56
|
+
channel=channel,
|
|
57
|
+
user=User(conn.user_id, conn.user_id),
|
|
58
|
+
message=MessageObject(id=message_id, content=message_content),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class WebsocketConnection:
|
|
63
|
+
connection: WebSocket
|
|
64
|
+
|
|
65
|
+
def __init__(self, adapter: WebUIAdapter, connection: WebSocket, session_id: str = "entari"):
|
|
66
|
+
self.adapter = adapter
|
|
67
|
+
self.connection = connection
|
|
68
|
+
self.session_id = session_id
|
|
69
|
+
self.user_id: str = f"user_{token_hex(8)}"
|
|
70
|
+
self.close_signal: asyncio.Event = asyncio.Event()
|
|
71
|
+
self.response_waiters: dict[str, asyncio.Future] = {}
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def alive(self) -> bool:
|
|
75
|
+
return not self.close_signal.is_set()
|
|
76
|
+
|
|
77
|
+
async def message_receive(self):
|
|
78
|
+
|
|
79
|
+
while True:
|
|
80
|
+
message = await self.connection.receive()
|
|
81
|
+
|
|
82
|
+
if message["type"] == "websocket.disconnect":
|
|
83
|
+
break
|
|
84
|
+
yield self, decode(message["text"])
|
|
85
|
+
self.close_signal.set()
|
|
86
|
+
|
|
87
|
+
async def message_handle(self):
|
|
88
|
+
async for _, data in self.message_receive():
|
|
89
|
+
if token := data.get("token"):
|
|
90
|
+
if token in self.response_waiters:
|
|
91
|
+
self.response_waiters[token].set_result(data)
|
|
92
|
+
continue
|
|
93
|
+
event_type = data.get("type")
|
|
94
|
+
if event_type == "message_create":
|
|
95
|
+
event = await message_receive(data["data"], self)
|
|
96
|
+
else:
|
|
97
|
+
event = Event(EventType.INTERNAL, datetime.now(), self.adapter.logins[self.session_id])
|
|
98
|
+
event._type = event_type
|
|
99
|
+
event._data = data.get("data")
|
|
100
|
+
asyncio.create_task(self.adapter.server.post(event))
|
|
101
|
+
|
|
102
|
+
async def connection_closed(self):
|
|
103
|
+
self.close_signal.set()
|
|
104
|
+
|
|
105
|
+
async def call_api(self, action: str, params: dict | None = None) -> dict:
|
|
106
|
+
future = asyncio.get_running_loop().create_future()
|
|
107
|
+
token = token_hex(16)
|
|
108
|
+
self.response_waiters[token] = future
|
|
109
|
+
try:
|
|
110
|
+
await self.send({"action": action, "data": params or {}, "token": token})
|
|
111
|
+
result = await asyncio.wait_for(future, timeout=60)
|
|
112
|
+
finally:
|
|
113
|
+
self.response_waiters.pop(token, None)
|
|
114
|
+
if result["status"] != "ok":
|
|
115
|
+
raise ActionFailed(result.get("error", "Unknown error"), result)
|
|
116
|
+
return result.get("data", {})
|
|
117
|
+
|
|
118
|
+
async def send(self, payload: dict) -> None:
|
|
119
|
+
return await self.connection.send_text(encode(payload))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def apply(adapter: WebUIAdapter):
|
|
123
|
+
@adapter.route(Api.MESSAGE_CREATE)
|
|
124
|
+
async def _message_create(request: Request[MessageParam]):
|
|
125
|
+
conn = adapter.connections[request.self_id]
|
|
126
|
+
channel_id = request.params["channel_id"]
|
|
127
|
+
if "elements" in request.params:
|
|
128
|
+
elements = request.params["elements"]
|
|
129
|
+
content = elements_to_content(elements)
|
|
130
|
+
else:
|
|
131
|
+
content = request.params["content"]
|
|
132
|
+
elements = [serialize_element(e) for e in parse(content)]
|
|
133
|
+
result = await conn.call_api("message_create", {"channel_id": channel_id, "elements": elements})
|
|
134
|
+
return [MessageObject(result["id"], content)]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class WebUIAdapter(BaseAdapter):
|
|
138
|
+
def __init__(self):
|
|
139
|
+
super().__init__()
|
|
140
|
+
self.connections: dict[str, WebsocketConnection] = {}
|
|
141
|
+
self.logins: dict[str, Login] = {}
|
|
142
|
+
|
|
143
|
+
apply(self)
|
|
144
|
+
|
|
145
|
+
def ensure(self, platform: str, self_id: str) -> bool:
|
|
146
|
+
return platform == "webui" and self_id in self.connections
|
|
147
|
+
|
|
148
|
+
async def get_logins(self) -> list[Login]:
|
|
149
|
+
logins = list(self.logins.values())
|
|
150
|
+
for index, login in enumerate(logins):
|
|
151
|
+
login.sn = index
|
|
152
|
+
return logins
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def required(self) -> set[str]:
|
|
156
|
+
return {"satori-python.server", "asgi.service/uvicorn"}
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def stages(self) -> set[Phase]:
|
|
160
|
+
return {"preparing", "blocking", "cleanup"}
|
|
161
|
+
|
|
162
|
+
def get_routes(self):
|
|
163
|
+
return [
|
|
164
|
+
WebSocketRoute("/api/chat", self.websocket_server_handler),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
async def websocket_server_handler(self, ws: WebSocket):
|
|
168
|
+
sid = None
|
|
169
|
+
if not is_local_mode():
|
|
170
|
+
sid = ws.cookies.get("webui_sid")
|
|
171
|
+
from entari_plugin_webui import _session_store
|
|
172
|
+
|
|
173
|
+
if _session_store.get(sid) is None:
|
|
174
|
+
await ws.close(code=1008, reason="Invalid session")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
sid = sid or f"entari_{token_hex(8)}"
|
|
178
|
+
if sid in self.connections:
|
|
179
|
+
old = self.connections[sid]
|
|
180
|
+
if old.alive:
|
|
181
|
+
await ws.close(code=1008, reason="Duplicate connection")
|
|
182
|
+
return
|
|
183
|
+
self.connections.pop(sid, None)
|
|
184
|
+
|
|
185
|
+
await ws.accept()
|
|
186
|
+
conn = WebsocketConnection(self, ws, session_id=sid)
|
|
187
|
+
self.connections[sid] = conn
|
|
188
|
+
|
|
189
|
+
if sid not in self.logins:
|
|
190
|
+
self.logins[sid] = Login(
|
|
191
|
+
platform="webui",
|
|
192
|
+
user=User(id=sid, name="Entari"),
|
|
193
|
+
status=LoginStatus.ONLINE,
|
|
194
|
+
)
|
|
195
|
+
await self.server.post(Event(EventType.LOGIN_ADDED, datetime.now(), self.logins[sid]))
|
|
196
|
+
else:
|
|
197
|
+
self.logins[sid].status = LoginStatus.ONLINE
|
|
198
|
+
await self.server.post(Event(EventType.LOGIN_UPDATED, datetime.now(), self.logins[sid]))
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
await any_completed(
|
|
202
|
+
conn.message_handle(),
|
|
203
|
+
conn.close_signal.wait(),
|
|
204
|
+
)
|
|
205
|
+
finally:
|
|
206
|
+
await conn.connection_closed()
|
|
207
|
+
self.connections.pop(sid, None)
|
|
208
|
+
if conn.response_waiters:
|
|
209
|
+
for future in conn.response_waiters.values():
|
|
210
|
+
future.cancel()
|
|
211
|
+
self.logins[sid].status = LoginStatus.OFFLINE
|
|
212
|
+
await self.server.post(Event(EventType.LOGIN_REMOVED, datetime.now(), self.logins[sid]))
|
|
213
|
+
|
|
214
|
+
def get_platform(self) -> str:
|
|
215
|
+
return "webui"
|
|
216
|
+
|
|
217
|
+
async def handle_internal(self, request: Request, path: str) -> Response:
|
|
218
|
+
if path.startswith("_api"):
|
|
219
|
+
self_id = request.self_id
|
|
220
|
+
return JSONResponse(await self.connections[self_id].call_api(path[5:], await request.origin.json()))
|
|
221
|
+
async with self.server.session.get(path) as resp:
|
|
222
|
+
return Response(await resp.read())
|
|
223
|
+
|
|
224
|
+
async def launch(self, manager: Launart):
|
|
225
|
+
async with self.stage("preparing"):
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
async with self.stage("blocking"):
|
|
229
|
+
await manager.status.wait_for_sigexit()
|
|
230
|
+
|
|
231
|
+
async with self.stage("cleanup"):
|
|
232
|
+
pass
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from starlette.responses import JSONResponse
|
|
6
|
+
|
|
7
|
+
from entari_plugin_webui import webui_config
|
|
8
|
+
|
|
9
|
+
from ..core.audit import audit
|
|
10
|
+
from ..core.error import AuthRequired, TooManyRequests
|
|
11
|
+
from ..core.security import (
|
|
12
|
+
hash_password,
|
|
13
|
+
is_local_mode,
|
|
14
|
+
verify_password,
|
|
15
|
+
)
|
|
16
|
+
from ..core.session import SessionStore
|
|
17
|
+
from .deps import get_session_store, require_auth
|
|
18
|
+
|
|
19
|
+
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
20
|
+
|
|
21
|
+
_COOKIE = "webui_sid"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LoginRequest(BaseModel):
|
|
25
|
+
password: str = Field(min_length=1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PasswordRequest(BaseModel):
|
|
29
|
+
old_password: str
|
|
30
|
+
new_password: str = Field(min_length=6)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _client_ip(request: Request) -> str:
|
|
34
|
+
return request.client.host if request.client else "unknown"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("/check")
|
|
38
|
+
async def check_auth():
|
|
39
|
+
return {"local_mode": is_local_mode(), "initialized": bool(webui_config.password)}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.post("/login")
|
|
43
|
+
async def login(body: LoginRequest, request: Request, store: SessionStore = Depends(get_session_store)):
|
|
44
|
+
ip = _client_ip(request)
|
|
45
|
+
from .. import _login_throttle # type: ignore # noqa: PLC0415
|
|
46
|
+
|
|
47
|
+
if is_local_mode():
|
|
48
|
+
sid = store.create(ip=ip)
|
|
49
|
+
return _json_with_cookie({"success": True, "local_mode": True}, sid)
|
|
50
|
+
|
|
51
|
+
if _login_throttle.is_limited(ip):
|
|
52
|
+
audit("login.failed", ip=ip, reason="rate_limited")
|
|
53
|
+
raise TooManyRequests(retry_after=_login_throttle.retry_after(ip), message="尝试过于频繁,请稍后再试")
|
|
54
|
+
|
|
55
|
+
if not webui_config.password:
|
|
56
|
+
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "管理员密码尚未初始化")
|
|
57
|
+
|
|
58
|
+
if not verify_password(body.password, webui_config.password):
|
|
59
|
+
_login_throttle.record_failure(ip)
|
|
60
|
+
audit("login.failed", ip=ip, reason="wrong_password")
|
|
61
|
+
raise AuthRequired(message="密码错误")
|
|
62
|
+
|
|
63
|
+
_login_throttle.reset(ip)
|
|
64
|
+
sid = store.create(ip=ip)
|
|
65
|
+
audit("login.success", ip=ip)
|
|
66
|
+
return _json_with_cookie({"success": True, "local_mode": False}, sid)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.post("/logout")
|
|
70
|
+
async def logout(request: Request, store: SessionStore = Depends(get_session_store)):
|
|
71
|
+
sid = request.cookies.get(_COOKIE)
|
|
72
|
+
if sid:
|
|
73
|
+
store.destroy(sid)
|
|
74
|
+
if not is_local_mode():
|
|
75
|
+
audit("logout", ip=_client_ip(request))
|
|
76
|
+
resp = JSONResponse({"success": True})
|
|
77
|
+
resp.delete_cookie(_COOKIE, path="/")
|
|
78
|
+
return resp
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.put("/password")
|
|
82
|
+
async def change_password(
|
|
83
|
+
body: PasswordRequest,
|
|
84
|
+
request: Request,
|
|
85
|
+
_sess=Depends(require_auth),
|
|
86
|
+
):
|
|
87
|
+
if not is_local_mode() and not verify_password(body.old_password, webui_config.password):
|
|
88
|
+
audit("password.change.failed", ip=_client_ip(request), reason="wrong_old")
|
|
89
|
+
raise AuthRequired(message="旧密码错误")
|
|
90
|
+
webui_config.password = hash_password(body.new_password)
|
|
91
|
+
audit("password.change", ip=_client_ip(request))
|
|
92
|
+
return {"success": True}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _json_with_cookie(payload: dict, sid: str) -> JSONResponse:
|
|
96
|
+
resp = JSONResponse(payload)
|
|
97
|
+
secure = True
|
|
98
|
+
resp.set_cookie(
|
|
99
|
+
_COOKIE,
|
|
100
|
+
sid,
|
|
101
|
+
httponly=True,
|
|
102
|
+
samesite="lax",
|
|
103
|
+
secure=secure,
|
|
104
|
+
max_age=webui_config.session_ttl,
|
|
105
|
+
path="/",
|
|
106
|
+
)
|
|
107
|
+
return resp
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from ..services.config_service import (
|
|
7
|
+
get_schema_for_section,
|
|
8
|
+
get_section,
|
|
9
|
+
list_sections,
|
|
10
|
+
update_section,
|
|
11
|
+
)
|
|
12
|
+
from .deps import require_auth
|
|
13
|
+
|
|
14
|
+
router = APIRouter(prefix="/api/config", tags=["config"], dependencies=[Depends(require_auth)])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SectionBody(BaseModel):
|
|
18
|
+
data: dict | list
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("/{section}/schema")
|
|
22
|
+
async def schema(section: str):
|
|
23
|
+
return {"success": True, "section": section, **get_schema_for_section(section)}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("")
|
|
27
|
+
async def list_():
|
|
28
|
+
return {"success": True, **list_sections()}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get("/{section}")
|
|
32
|
+
async def get_(section: str):
|
|
33
|
+
return {"success": True, "section": section, "data": get_section(section)}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.put("/{section}")
|
|
37
|
+
async def put_(section: str, body: SectionBody):
|
|
38
|
+
update_section(section, body.data)
|
|
39
|
+
return {"success": True, "message": "已保存"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, Request
|
|
4
|
+
|
|
5
|
+
from ..core.error import AuthRequired
|
|
6
|
+
from ..core.security import is_local_mode
|
|
7
|
+
from ..core.session import Session, SessionStore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_session_store() -> SessionStore:
|
|
11
|
+
from .. import _session_store # type: ignore # noqa: PLC0415
|
|
12
|
+
|
|
13
|
+
return _session_store
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def require_auth(request: Request, store: SessionStore = Depends(get_session_store)) -> Session | None:
|
|
17
|
+
if is_local_mode():
|
|
18
|
+
return None
|
|
19
|
+
sid = request.cookies.get("webui_sid")
|
|
20
|
+
sess = store.get(sid)
|
|
21
|
+
if sess is None:
|
|
22
|
+
raise AuthRequired("未登录或会话已过期")
|
|
23
|
+
store.refresh_if_needed(sess)
|
|
24
|
+
return sess
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
|
|
5
|
+
from ..api.deps import require_auth
|
|
6
|
+
from ..core.extension import get_all_menus, get_all_pages
|
|
7
|
+
from ..core.i18n import I18nRegistry
|
|
8
|
+
from ..core.permissions import get_permissions
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/api/extensions", tags=["extensions"], dependencies=[Depends(require_auth)])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/manifest")
|
|
14
|
+
async def manifest():
|
|
15
|
+
return {
|
|
16
|
+
"success": True,
|
|
17
|
+
"menus": get_all_menus(),
|
|
18
|
+
"pages": get_all_pages(),
|
|
19
|
+
"i18n": I18nRegistry.all(),
|
|
20
|
+
"permissions": get_permissions(),
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/api", tags=["health"])
|
|
9
|
+
|
|
10
|
+
_START = datetime.now()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/health")
|
|
14
|
+
async def health() -> dict:
|
|
15
|
+
|
|
16
|
+
frontend_built = (Path(__file__).resolve().parent.parent / "static" / "frontend" / "index.html").exists()
|
|
17
|
+
return {
|
|
18
|
+
"status": "ok",
|
|
19
|
+
"uptime_seconds": int((datetime.now() - _START).total_seconds()),
|
|
20
|
+
"frontend_built": frontend_built,
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from entari_plugin_server import add_websocket_route
|
|
6
|
+
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
7
|
+
|
|
8
|
+
from ..core.log_stream import get_log_buffer
|
|
9
|
+
from ..core.security import is_local_mode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@add_websocket_route("/ws/logs")
|
|
13
|
+
async def websocket_logs(ws: WebSocket) -> None:
|
|
14
|
+
if not is_local_mode():
|
|
15
|
+
sid = ws.cookies.get("webui_sid")
|
|
16
|
+
from entari_plugin_webui import _session_store
|
|
17
|
+
|
|
18
|
+
if _session_store.get(sid) is None:
|
|
19
|
+
await ws.close(code=1008)
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
await ws.accept()
|
|
23
|
+
buffer = get_log_buffer()
|
|
24
|
+
history_data, last_pos = buffer.get_recent(100)
|
|
25
|
+
await ws.send_json({"type": "history", "data": history_data})
|
|
26
|
+
try:
|
|
27
|
+
while True:
|
|
28
|
+
text, last_pos = buffer.get_new_since(last_pos)
|
|
29
|
+
if text:
|
|
30
|
+
await ws.send_json({"type": "log", "data": text})
|
|
31
|
+
await asyncio.sleep(0.5)
|
|
32
|
+
except (WebSocketDisconnect, asyncio.CancelledError):
|
|
33
|
+
pass
|
|
34
|
+
except Exception: # noqa: BLE001
|
|
35
|
+
pass
|
|
36
|
+
finally:
|
|
37
|
+
from contextlib import suppress
|
|
38
|
+
|
|
39
|
+
with suppress(Exception):
|
|
40
|
+
await ws.close()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from starlette.status import HTTP_404_NOT_FOUND
|
|
7
|
+
|
|
8
|
+
from ..api.deps import require_auth
|
|
9
|
+
from ..core.error import PluginNotFound
|
|
10
|
+
from ..services.market_service import (
|
|
11
|
+
get_plugin,
|
|
12
|
+
get_task,
|
|
13
|
+
list_plugins,
|
|
14
|
+
start_install,
|
|
15
|
+
start_uninstall,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix="/api/market", tags=["market"], dependencies=[Depends(require_auth)])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NameBody(BaseModel):
|
|
22
|
+
name: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/plugins")
|
|
26
|
+
async def list_():
|
|
27
|
+
return {"success": True, **(await list_plugins())}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get("/plugins/{name}")
|
|
31
|
+
async def detail(name: str):
|
|
32
|
+
p = await get_plugin(name)
|
|
33
|
+
if p is None:
|
|
34
|
+
raise PluginNotFound(name)
|
|
35
|
+
return {"success": True, "data": p}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.post("/install")
|
|
39
|
+
async def install(body: NameBody):
|
|
40
|
+
tid = await start_install(body.name)
|
|
41
|
+
return {"success": True, "task_id": tid}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router.post("/uninstall")
|
|
45
|
+
async def uninstall(body: NameBody):
|
|
46
|
+
tid = await start_uninstall(body.name)
|
|
47
|
+
return {"success": True, "task_id": tid}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.get("/tasks/{task_id}")
|
|
51
|
+
async def task(task_id: str):
|
|
52
|
+
t = get_task(task_id)
|
|
53
|
+
if t is None:
|
|
54
|
+
return JSONResponse({"success": False, "code": "task_not_found"}, status_code=HTTP_404_NOT_FOUND)
|
|
55
|
+
return {
|
|
56
|
+
"success": True,
|
|
57
|
+
"task_id": t.task_id,
|
|
58
|
+
"pip_name": t.pip_name,
|
|
59
|
+
"action": t.action,
|
|
60
|
+
"status": t.status,
|
|
61
|
+
"percent": t.percent,
|
|
62
|
+
"message": t.message,
|
|
63
|
+
}
|