nonebot-plugin-shiro-web-console 0.1.5__py3-none-any.whl → 0.1.7__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.
Potentially problematic release.
This version of nonebot-plugin-shiro-web-console might be problematic. Click here for more details.
- nonebot_plugin_shiro_web_console/__init__.py +588 -480
- nonebot_plugin_shiro_web_console/static/index.html +55 -17
- {nonebot_plugin_shiro_web_console-0.1.5.dist-info → nonebot_plugin_shiro_web_console-0.1.7.dist-info}/METADATA +1 -2
- nonebot_plugin_shiro_web_console-0.1.7.dist-info/RECORD +7 -0
- nonebot_plugin_shiro_web_console-0.1.5.dist-info/RECORD +0 -7
- {nonebot_plugin_shiro_web_console-0.1.5.dist-info → nonebot_plugin_shiro_web_console-0.1.7.dist-info}/WHEEL +0 -0
- {nonebot_plugin_shiro_web_console-0.1.5.dist-info → nonebot_plugin_shiro_web_console-0.1.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,6 +4,9 @@ import httpx
|
|
|
4
4
|
import os
|
|
5
5
|
import random
|
|
6
6
|
import time
|
|
7
|
+
import secrets
|
|
8
|
+
import hashlib
|
|
9
|
+
import re
|
|
7
10
|
from datetime import datetime, timedelta
|
|
8
11
|
from urllib.parse import quote, unquote
|
|
9
12
|
from typing import Dict, List, Set, Optional, Any
|
|
@@ -13,14 +16,15 @@ from collections import deque
|
|
|
13
16
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, Response, Depends, HTTPException
|
|
14
17
|
from fastapi.staticfiles import StaticFiles
|
|
15
18
|
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
|
16
|
-
from nonebot import get_app, get_bot, get_driver, logger, on_message, on_command, require
|
|
17
|
-
require("nonebot_plugin_localstore")
|
|
19
|
+
from nonebot import get_app, get_bot, get_bots, get_driver, logger, on_message, on_command, require
|
|
18
20
|
import nonebot_plugin_localstore
|
|
19
21
|
from .config import Config, config
|
|
20
22
|
from nonebot.permission import SUPERUSER
|
|
21
23
|
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent, PrivateMessageEvent, MessageSegment
|
|
22
24
|
from nonebot.plugin import PluginMetadata
|
|
23
25
|
|
|
26
|
+
START_TIME = time.time()
|
|
27
|
+
|
|
24
28
|
__plugin_meta__ = PluginMetadata(
|
|
25
29
|
name="Shiro Web Console",
|
|
26
30
|
description="通过浏览器查看日志、管理机器人并发送消息",
|
|
@@ -31,20 +35,43 @@ __plugin_meta__ = PluginMetadata(
|
|
|
31
35
|
supported_adapters={"~onebot.v11"},
|
|
32
36
|
extra={
|
|
33
37
|
"author": "luojisama",
|
|
34
|
-
"version": "0.1.
|
|
38
|
+
"version": "0.1.7",
|
|
35
39
|
"pypi_test": "nonebot-plugin-shiro-web-console",
|
|
36
40
|
},
|
|
37
41
|
)
|
|
38
42
|
|
|
43
|
+
# WebSocket 连接池
|
|
44
|
+
active_connections: Set[WebSocket] = set()
|
|
45
|
+
|
|
46
|
+
async def broadcast_message(data: dict):
|
|
47
|
+
if not active_connections:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
dead_connections = set()
|
|
51
|
+
for ws in active_connections:
|
|
52
|
+
try:
|
|
53
|
+
await ws.send_json(data)
|
|
54
|
+
except Exception:
|
|
55
|
+
dead_connections.add(ws)
|
|
56
|
+
|
|
57
|
+
for ws in dead_connections:
|
|
58
|
+
active_connections.remove(ws)
|
|
59
|
+
|
|
39
60
|
# 日志缓冲区,保留最近 200 条日志
|
|
40
61
|
log_buffer = deque(maxlen=200)
|
|
41
62
|
|
|
42
|
-
def log_sink(message):
|
|
43
|
-
|
|
63
|
+
async def log_sink(message):
|
|
64
|
+
log_entry = {
|
|
44
65
|
"time": datetime.now().strftime("%H:%M:%S"),
|
|
45
66
|
"level": message.record["level"].name,
|
|
46
67
|
"message": message.record["message"],
|
|
47
68
|
"module": message.record["module"]
|
|
69
|
+
}
|
|
70
|
+
log_buffer.append(log_entry)
|
|
71
|
+
# 推送日志
|
|
72
|
+
await broadcast_message({
|
|
73
|
+
"type": "new_log",
|
|
74
|
+
"data": log_entry
|
|
48
75
|
})
|
|
49
76
|
|
|
50
77
|
# 注册 loguru sink
|
|
@@ -63,20 +90,30 @@ class AuthManager:
|
|
|
63
90
|
self.password_file = self.data_dir / "password.json"
|
|
64
91
|
|
|
65
92
|
# 初始加载密码
|
|
66
|
-
self.
|
|
93
|
+
self.admin_password_hash = self._load_password_hash()
|
|
67
94
|
|
|
68
|
-
def
|
|
95
|
+
def _load_password_hash(self) -> str:
|
|
96
|
+
pwd = "admin123"
|
|
69
97
|
if self.password_file.exists():
|
|
70
98
|
try:
|
|
71
99
|
data = json.loads(self.password_file.read_text(encoding="utf-8"))
|
|
72
|
-
|
|
100
|
+
if "password_hash" in data:
|
|
101
|
+
return data["password_hash"]
|
|
102
|
+
pwd = data.get("password", "admin123")
|
|
73
103
|
except:
|
|
74
104
|
pass
|
|
75
|
-
|
|
105
|
+
else:
|
|
106
|
+
pwd = config.web_console_password
|
|
107
|
+
|
|
108
|
+
# 迁移或初始化:将明文转换为哈希
|
|
109
|
+
return hashlib.sha256(pwd.encode()).hexdigest()
|
|
76
110
|
|
|
77
111
|
def save_password(self, new_password: str):
|
|
78
|
-
|
|
79
|
-
self.
|
|
112
|
+
pwd_hash = hashlib.sha256(new_password.encode()).hexdigest()
|
|
113
|
+
self.admin_password_hash = pwd_hash
|
|
114
|
+
self.password_file.write_text(json.dumps({"password_hash": pwd_hash}), encoding="utf-8")
|
|
115
|
+
# 修改密码后使旧 token 失效
|
|
116
|
+
self.token = None
|
|
80
117
|
|
|
81
118
|
def generate_code(self) -> str:
|
|
82
119
|
self.code = "".join([str(random.randint(0, 9)) for _ in range(6)])
|
|
@@ -96,13 +133,14 @@ class AuthManager:
|
|
|
96
133
|
return False
|
|
97
134
|
|
|
98
135
|
def verify_password(self, password: str) -> bool:
|
|
99
|
-
|
|
136
|
+
input_hash = hashlib.sha256(password.encode()).hexdigest()
|
|
137
|
+
if input_hash == self.admin_password_hash:
|
|
100
138
|
self.generate_token()
|
|
101
139
|
return True
|
|
102
140
|
return False
|
|
103
141
|
|
|
104
142
|
def generate_token(self):
|
|
105
|
-
self.token =
|
|
143
|
+
self.token = secrets.token_hex(16)
|
|
106
144
|
self.token_expire = datetime.now() + timedelta(days=7)
|
|
107
145
|
|
|
108
146
|
def verify_token(self, token: str) -> bool:
|
|
@@ -126,29 +164,36 @@ async def check_auth(request: Request):
|
|
|
126
164
|
return True
|
|
127
165
|
|
|
128
166
|
try:
|
|
129
|
-
|
|
130
|
-
except ValueError:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
#
|
|
149
|
-
@app.get("/web_console
|
|
150
|
-
async def
|
|
151
|
-
|
|
167
|
+
_app = get_app()
|
|
168
|
+
except (ValueError, AssertionError):
|
|
169
|
+
_app = None
|
|
170
|
+
|
|
171
|
+
# 只有在驱动器支持 FastAPI 时才挂载
|
|
172
|
+
if isinstance(_app, FastAPI):
|
|
173
|
+
app = _app
|
|
174
|
+
else:
|
|
175
|
+
app = None
|
|
176
|
+
logger.warning("驱动器不支持 FastAPI,Web 控制台路由将无法访问。")
|
|
177
|
+
|
|
178
|
+
if app:
|
|
179
|
+
static_path = Path(__file__).parent / "static"
|
|
180
|
+
index_html = static_path / "index.html"
|
|
181
|
+
|
|
182
|
+
# 挂载静态文件
|
|
183
|
+
if static_path.exists():
|
|
184
|
+
app.mount("/web_console/static", StaticFiles(directory=str(static_path)), name="web_console_static")
|
|
185
|
+
|
|
186
|
+
# Web 控制台入口路由
|
|
187
|
+
@app.get("/web_console", response_class=HTMLResponse)
|
|
188
|
+
async def serve_console():
|
|
189
|
+
if not index_html.exists():
|
|
190
|
+
return HTMLResponse("<h1>index.html not found</h1>", status_code=404)
|
|
191
|
+
return HTMLResponse(content=index_html.read_text(encoding="utf-8"), status_code=200)
|
|
192
|
+
|
|
193
|
+
# 兼容 /web_console/ 路径
|
|
194
|
+
@app.get("/web_console/", response_class=HTMLResponse)
|
|
195
|
+
async def serve_console_slash():
|
|
196
|
+
return await serve_console()
|
|
152
197
|
|
|
153
198
|
# 消息缓存 {chat_id: [messages]}
|
|
154
199
|
message_cache: Dict[str, List[dict]] = {}
|
|
@@ -157,7 +202,8 @@ image_cache: Dict[str, dict] = {}
|
|
|
157
202
|
CACHE_SIZE = 100
|
|
158
203
|
|
|
159
204
|
# WebSocket 连接池
|
|
160
|
-
active_connections
|
|
205
|
+
# active_connections defined at top
|
|
206
|
+
|
|
161
207
|
|
|
162
208
|
# 基础人设
|
|
163
209
|
def get_chat_id(event: MessageEvent) -> str:
|
|
@@ -176,7 +222,7 @@ async def handle_password_cmd(bot: Bot, event: MessageEvent):
|
|
|
176
222
|
await password_cmd.finish("请在命令后输入新密码,例如:web密码 mynewpassword")
|
|
177
223
|
|
|
178
224
|
auth_manager.save_password(new_password)
|
|
179
|
-
await password_cmd.finish(f"Web
|
|
225
|
+
await password_cmd.finish(f"Web控制台密码已修改。\n请妥善保存。")
|
|
180
226
|
|
|
181
227
|
@login_cmd.handle()
|
|
182
228
|
async def handle_login_cmd(bot: Bot, event: MessageEvent):
|
|
@@ -363,481 +409,543 @@ async def handle_all_messages(bot: Bot, event: MessageEvent):
|
|
|
363
409
|
"data": msg_data
|
|
364
410
|
})
|
|
365
411
|
|
|
366
|
-
async def broadcast_message(data: dict):
|
|
367
|
-
if not active_connections:
|
|
368
|
-
return
|
|
369
|
-
|
|
370
|
-
dead_connections = set()
|
|
371
|
-
for ws in active_connections:
|
|
372
|
-
try:
|
|
373
|
-
await ws.send_json(data)
|
|
374
|
-
except Exception:
|
|
375
|
-
dead_connections.add(ws)
|
|
376
|
-
|
|
377
|
-
for ws in dead_connections:
|
|
378
|
-
active_connections.remove(ws)
|
|
379
412
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
code = auth_manager.generate_code()
|
|
387
|
-
bot = get_bot()
|
|
388
|
-
|
|
389
|
-
success_count = 0
|
|
390
|
-
for user_id in superusers:
|
|
391
|
-
try:
|
|
392
|
-
await bot.send_private_msg(user_id=int(user_id), message=f"【Web控制台】您的登录验证码为:{code},5分钟内有效。")
|
|
393
|
-
success_count += 1
|
|
394
|
-
except Exception as e:
|
|
395
|
-
logger.error(f"发送验证码给管理员 {user_id} 失败: {e}")
|
|
396
|
-
|
|
397
|
-
if success_count > 0:
|
|
398
|
-
return {"msg": "验证码已发送至管理员 QQ"}
|
|
399
|
-
return {"error": "验证码发送失败,请检查机器人是否在线或管理员账号是否正确"}
|
|
400
|
-
|
|
401
|
-
@app.post("/web_console/api/login")
|
|
402
|
-
async def login(data: dict):
|
|
403
|
-
code = data.get("code")
|
|
404
|
-
password = data.get("password")
|
|
405
|
-
|
|
406
|
-
if code:
|
|
407
|
-
if auth_manager.verify_code(code):
|
|
408
|
-
return {"token": auth_manager.token}
|
|
409
|
-
return {"error": "验证码错误或已过期", "code": 401}
|
|
410
|
-
elif password:
|
|
411
|
-
if auth_manager.verify_password(password):
|
|
412
|
-
return {"token": auth_manager.token}
|
|
413
|
-
return {"error": "密码错误", "code": 401}
|
|
413
|
+
if app:
|
|
414
|
+
# 认证 API
|
|
415
|
+
@app.post("/web_console/api/send_code")
|
|
416
|
+
async def send_code():
|
|
417
|
+
if not superusers:
|
|
418
|
+
return {"error": "未设置 SUPERUSERS 管理员列表"}
|
|
414
419
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
@app.get("/web_console/api/status", dependencies=[Depends(check_auth)])
|
|
418
|
-
async def get_system_status():
|
|
419
|
-
from nonebot import get_bots
|
|
420
|
-
import psutil
|
|
421
|
-
import platform
|
|
422
|
-
import time
|
|
423
|
-
import datetime
|
|
424
|
-
|
|
425
|
-
# 系统性能
|
|
426
|
-
cpu_percent = psutil.cpu_percent()
|
|
427
|
-
memory = psutil.virtual_memory()
|
|
428
|
-
disk = psutil.disk_usage('/')
|
|
429
|
-
|
|
430
|
-
# 网络流量
|
|
431
|
-
net_io = psutil.net_io_counters()
|
|
432
|
-
|
|
433
|
-
# 运行时间
|
|
434
|
-
boot_time = psutil.boot_time()
|
|
435
|
-
uptime = time.time() - boot_time
|
|
436
|
-
uptime_str = str(datetime.timedelta(seconds=int(uptime)))
|
|
437
|
-
|
|
438
|
-
# 机器人信息
|
|
439
|
-
bots_info = []
|
|
440
|
-
for bot_id, bot in get_bots().items():
|
|
441
|
-
try:
|
|
442
|
-
profile = await bot.get_login_info()
|
|
443
|
-
bots_info.append({
|
|
444
|
-
"id": bot_id,
|
|
445
|
-
"nickname": profile.get("nickname", "未知"),
|
|
446
|
-
"avatar": f"https://q.qlogo.cn/headimg_dl?dst_uin={bot_id}&spec=640",
|
|
447
|
-
"status": "在线"
|
|
448
|
-
})
|
|
449
|
-
except:
|
|
450
|
-
bots_info.append({
|
|
451
|
-
"id": bot_id,
|
|
452
|
-
"nickname": "机器人",
|
|
453
|
-
"avatar": f"https://q.qlogo.cn/headimg_dl?dst_uin={bot_id}&spec=640",
|
|
454
|
-
"status": "离线"
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
return {
|
|
458
|
-
"system": {
|
|
459
|
-
"os": platform.system(),
|
|
460
|
-
"cpu": f"{cpu_percent}%",
|
|
461
|
-
"memory": f"{memory.percent}%",
|
|
462
|
-
"memory_used": f"{round(memory.used / 1024 / 1024 / 1024, 2)} GB",
|
|
463
|
-
"memory_total": f"{round(memory.total / 1024 / 1024 / 1024, 2)} GB",
|
|
464
|
-
"disk": f"{disk.percent}%",
|
|
465
|
-
"disk_used": f"{round(disk.used / 1024 / 1024 / 1024, 2)} GB",
|
|
466
|
-
"disk_total": f"{round(disk.total / 1024 / 1024 / 1024, 2)} GB",
|
|
467
|
-
"net_sent": f"{round(net_io.bytes_sent / 1024 / 1024, 2)} MB",
|
|
468
|
-
"net_recv": f"{round(net_io.bytes_recv / 1024 / 1024, 2)} MB",
|
|
469
|
-
"uptime": uptime_str,
|
|
470
|
-
"python": platform.python_version()
|
|
471
|
-
},
|
|
472
|
-
"bots": bots_info
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
@app.get("/web_console/api/logs", dependencies=[Depends(check_auth)])
|
|
476
|
-
async def get_logs():
|
|
477
|
-
return list(log_buffer)
|
|
478
|
-
|
|
479
|
-
@app.get("/web_console/api/plugins", dependencies=[Depends(check_auth)])
|
|
480
|
-
async def get_plugins():
|
|
481
|
-
from nonebot import get_loaded_plugins
|
|
482
|
-
import os
|
|
483
|
-
plugins = []
|
|
484
|
-
for p in get_loaded_plugins():
|
|
485
|
-
metadata = p.metadata
|
|
420
|
+
code = auth_manager.generate_code()
|
|
486
421
|
|
|
487
|
-
#
|
|
488
|
-
|
|
489
|
-
|
|
422
|
+
# 兼容多 Bot 场景
|
|
423
|
+
from nonebot import get_bots
|
|
424
|
+
bots = get_bots()
|
|
425
|
+
if not bots:
|
|
426
|
+
return {"error": "未连接任何 Bot"}
|
|
427
|
+
bot = list(bots.values())[0]
|
|
428
|
+
|
|
429
|
+
success_count = 0
|
|
430
|
+
for user_id in superusers:
|
|
431
|
+
try:
|
|
432
|
+
await bot.send_private_msg(user_id=int(user_id), message=f"【Web控制台】您的登录验证码为:{code},5分钟内有效。")
|
|
433
|
+
success_count += 1
|
|
434
|
+
except Exception as e:
|
|
435
|
+
logger.error(f"发送验证码给管理员 {user_id} 失败: {e}")
|
|
436
|
+
|
|
437
|
+
if success_count > 0:
|
|
438
|
+
return {"msg": "验证码已发送至管理员 QQ"}
|
|
439
|
+
return {"error": "验证码发送失败,请检查机器人是否在线或管理员账号是否正确"}
|
|
440
|
+
|
|
441
|
+
@app.post("/web_console/api/login")
|
|
442
|
+
async def login(data: dict):
|
|
443
|
+
code = data.get("code")
|
|
444
|
+
password = data.get("password")
|
|
490
445
|
|
|
491
|
-
if
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
elif
|
|
496
|
-
|
|
446
|
+
if code:
|
|
447
|
+
if auth_manager.verify_code(code):
|
|
448
|
+
return {"token": auth_manager.token}
|
|
449
|
+
return {"error": "验证码错误或已过期", "code": 401}
|
|
450
|
+
elif password:
|
|
451
|
+
if auth_manager.verify_password(password):
|
|
452
|
+
return {"token": auth_manager.token}
|
|
453
|
+
return {"error": "密码错误", "code": 401}
|
|
497
454
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return plugins
|
|
508
|
-
|
|
509
|
-
@app.post("/web_console/api/system/action", dependencies=[Depends(check_auth)])
|
|
510
|
-
async def system_action(request: Request):
|
|
511
|
-
data = await request.json()
|
|
512
|
-
action = data.get("action")
|
|
513
|
-
|
|
514
|
-
if action not in ["reboot", "shutdown"]:
|
|
515
|
-
return {"error": "无效操作"}
|
|
455
|
+
return {"error": "请输入验证码或密码", "code": 400}
|
|
456
|
+
|
|
457
|
+
@app.get("/web_console/api/status", dependencies=[Depends(check_auth)])
|
|
458
|
+
async def get_system_status():
|
|
459
|
+
from nonebot import get_bots
|
|
460
|
+
import psutil
|
|
461
|
+
import platform
|
|
462
|
+
import time
|
|
463
|
+
import datetime
|
|
516
464
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
logger.warning(f"收到系统指令: {action}")
|
|
523
|
-
|
|
524
|
-
if action == "shutdown":
|
|
525
|
-
# 延迟执行关闭,确保响应能发出去
|
|
526
|
-
loop = asyncio.get_event_loop()
|
|
527
|
-
loop.call_later(1.0, lambda: os._exit(0))
|
|
528
|
-
return {"msg": "Bot 正在关闭..."}
|
|
465
|
+
# 系统性能
|
|
466
|
+
cpu_percent = psutil.cpu_percent()
|
|
467
|
+
memory = psutil.virtual_memory()
|
|
468
|
+
disk = psutil.disk_usage('.')
|
|
529
469
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
533
|
-
bot_py = os.path.join(root_dir, "bot.py")
|
|
470
|
+
# 网络流量
|
|
471
|
+
net_io = psutil.net_io_counters()
|
|
534
472
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
473
|
+
# 运行时间
|
|
474
|
+
uptime = time.time() - START_TIME
|
|
475
|
+
uptime_str = str(datetime.timedelta(seconds=int(uptime)))
|
|
476
|
+
|
|
477
|
+
# 机器人信息
|
|
478
|
+
bots_info = []
|
|
479
|
+
for bot_id, bot in get_bots().items():
|
|
541
480
|
try:
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
481
|
+
profile = await bot.get_login_info()
|
|
482
|
+
bots_info.append({
|
|
483
|
+
"id": bot_id,
|
|
484
|
+
"nickname": profile.get("nickname", "未知"),
|
|
485
|
+
"avatar": f"https://q.qlogo.cn/headimg_dl?dst_uin={bot_id}&spec=640",
|
|
486
|
+
"status": "在线"
|
|
487
|
+
})
|
|
488
|
+
except:
|
|
489
|
+
bots_info.append({
|
|
490
|
+
"id": bot_id,
|
|
491
|
+
"nickname": "机器人",
|
|
492
|
+
"avatar": f"https://q.qlogo.cn/headimg_dl?dst_uin={bot_id}&spec=640",
|
|
493
|
+
"status": "离线"
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
"system": {
|
|
498
|
+
"os": platform.system(),
|
|
499
|
+
"cpu": f"{cpu_percent}%",
|
|
500
|
+
"memory": f"{memory.percent}%",
|
|
501
|
+
"memory_used": f"{round(memory.used / 1024 / 1024 / 1024, 2)} GB",
|
|
502
|
+
"memory_total": f"{round(memory.total / 1024 / 1024 / 1024, 2)} GB",
|
|
503
|
+
"disk": f"{disk.percent}%",
|
|
504
|
+
"disk_used": f"{round(disk.used / 1024 / 1024 / 1024, 2)} GB",
|
|
505
|
+
"disk_total": f"{round(disk.total / 1024 / 1024 / 1024, 2)} GB",
|
|
506
|
+
"net_sent": f"{round(net_io.bytes_sent / 1024 / 1024, 2)} MB",
|
|
507
|
+
"net_recv": f"{round(net_io.bytes_recv / 1024 / 1024, 2)} MB",
|
|
508
|
+
"uptime": uptime_str,
|
|
509
|
+
"python": platform.python_version()
|
|
510
|
+
},
|
|
511
|
+
"bots": bots_info
|
|
512
|
+
}
|
|
551
513
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
514
|
+
@app.get("/web_console/api/logs", dependencies=[Depends(check_auth)])
|
|
515
|
+
async def get_logs():
|
|
516
|
+
return list(log_buffer)
|
|
517
|
+
|
|
518
|
+
@app.get("/web_console/api/plugins", dependencies=[Depends(check_auth)])
|
|
519
|
+
async def get_plugins():
|
|
520
|
+
from nonebot import get_loaded_plugins
|
|
521
|
+
import os
|
|
522
|
+
plugins = []
|
|
523
|
+
for p in get_loaded_plugins():
|
|
524
|
+
metadata = p.metadata
|
|
525
|
+
|
|
526
|
+
# 识别插件来源
|
|
527
|
+
plugin_type = "local"
|
|
528
|
+
module_name = p.module_name
|
|
529
|
+
|
|
530
|
+
if module_name.startswith("nonebot.plugins"):
|
|
531
|
+
plugin_type = "builtin"
|
|
532
|
+
elif metadata and metadata.homepage and ("github.com/nonebot" in metadata.homepage or "nonebot.dev" in metadata.homepage):
|
|
533
|
+
plugin_type = "official"
|
|
534
|
+
elif module_name.startswith("nonebot_plugin_"):
|
|
535
|
+
plugin_type = "store"
|
|
536
|
+
|
|
537
|
+
plugins.append({
|
|
538
|
+
"id": p.name,
|
|
539
|
+
"name": metadata.name if metadata else p.name,
|
|
540
|
+
"description": metadata.description if metadata else "暂无描述",
|
|
541
|
+
"version": metadata.extra.get("version", "1.0.0") if metadata and metadata.extra else "1.0.0",
|
|
542
|
+
"type": plugin_type,
|
|
543
|
+
"module": module_name,
|
|
544
|
+
"homepage": metadata.homepage if metadata else None
|
|
545
|
+
})
|
|
546
|
+
return plugins
|
|
556
547
|
|
|
557
|
-
@app.
|
|
558
|
-
async def
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
548
|
+
@app.post("/web_console/api/system/action", dependencies=[Depends(check_auth)])
|
|
549
|
+
async def system_action(request: Request):
|
|
550
|
+
data = await request.json()
|
|
551
|
+
action = data.get("action")
|
|
552
|
+
confirm = data.get("confirm")
|
|
553
|
+
|
|
554
|
+
if action not in ["reboot", "shutdown"]:
|
|
555
|
+
return {"error": "无效操作"}
|
|
556
|
+
|
|
557
|
+
if not confirm:
|
|
558
|
+
return {"error": "请确认操作", "need_confirm": True}
|
|
567
559
|
|
|
568
|
-
|
|
569
|
-
|
|
560
|
+
import os
|
|
561
|
+
import sys
|
|
562
|
+
import subprocess
|
|
563
|
+
import asyncio
|
|
570
564
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if hasattr(config_class, "schema"):
|
|
579
|
-
schema = config_class.schema()
|
|
580
|
-
config_schema = schema.get("properties", {})
|
|
581
|
-
# 注入当前值
|
|
582
|
-
driver_config = get_driver().config
|
|
583
|
-
for key in config_schema:
|
|
584
|
-
current_config[key] = getattr(driver_config, key, None)
|
|
585
|
-
except Exception as e:
|
|
586
|
-
logger.error(f"解析插件 {plugin_id} 配置失败: {e}")
|
|
565
|
+
logger.warning(f"收到系统指令: {action}")
|
|
566
|
+
|
|
567
|
+
if action == "shutdown":
|
|
568
|
+
# 延迟执行关闭,确保响应能发出去
|
|
569
|
+
loop = asyncio.get_event_loop()
|
|
570
|
+
loop.call_later(1.0, lambda: os._exit(0))
|
|
571
|
+
return {"msg": "Bot 正在关闭..."}
|
|
587
572
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
STORE_URL = "https://registry.nonebot.dev/plugins.json"
|
|
593
|
-
store_cache = {"data": [], "time": 0}
|
|
594
|
-
|
|
595
|
-
@app.get("/web_console/api/store", dependencies=[Depends(check_auth)])
|
|
596
|
-
async def get_store():
|
|
597
|
-
# 缓存 1 小时
|
|
598
|
-
if not store_cache["data"] or time.time() - store_cache["time"] > 3600:
|
|
599
|
-
try:
|
|
600
|
-
async with httpx.AsyncClient(follow_redirects=True, verify=False) as client:
|
|
601
|
-
resp = await client.get(STORE_URL, timeout=15.0)
|
|
602
|
-
if resp.status_code == 200:
|
|
603
|
-
store_cache["data"] = resp.json()
|
|
604
|
-
store_cache["time"] = time.time()
|
|
605
|
-
else:
|
|
606
|
-
logger.error(f"获取 NoneBot 商店数据失败: HTTP {resp.status_code}")
|
|
607
|
-
except Exception as e:
|
|
608
|
-
logger.error(f"获取 NoneBot 商店数据失败: {e}")
|
|
609
|
-
# 如果之前有缓存,即使失败也返回旧缓存,避免页面空白
|
|
610
|
-
if store_cache["data"]:
|
|
611
|
-
return store_cache["data"]
|
|
612
|
-
return {"error": "无法连接到 NoneBot 商店,请检查服务器网络或稍后再试"}
|
|
573
|
+
elif action == "reboot":
|
|
574
|
+
# 获取项目根目录 (通常是当前工作目录)
|
|
575
|
+
root_dir = Path.cwd()
|
|
576
|
+
bot_py = root_dir / "bot.py"
|
|
613
577
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
578
|
+
if bot_py.exists():
|
|
579
|
+
cmd = [sys.executable, str(bot_py)]
|
|
580
|
+
else:
|
|
581
|
+
cmd = [sys.executable] + sys.argv
|
|
582
|
+
|
|
583
|
+
def do_reboot():
|
|
584
|
+
try:
|
|
585
|
+
if sys.platform == "win32":
|
|
586
|
+
subprocess.Popen(cmd, cwd=str(root_dir))
|
|
587
|
+
os._exit(0)
|
|
588
|
+
else:
|
|
589
|
+
os.chdir(root_dir)
|
|
590
|
+
os.execv(sys.executable, cmd)
|
|
591
|
+
except Exception as e:
|
|
592
|
+
logger.error(f"重启执行失败: {e}")
|
|
593
|
+
os._exit(1)
|
|
594
|
+
|
|
595
|
+
# 延迟执行重启
|
|
596
|
+
loop = asyncio.get_event_loop()
|
|
597
|
+
loop.call_later(1.0, do_reboot)
|
|
598
|
+
return {"msg": "Bot 正在重启..."}
|
|
599
|
+
|
|
600
|
+
@app.get("/web_console/api/plugins/{plugin_id}/config", dependencies=[Depends(check_auth)])
|
|
601
|
+
async def get_plugin_config(plugin_id: str):
|
|
602
|
+
from nonebot import get_loaded_plugins, get_driver
|
|
624
603
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
604
|
+
# 查找插件
|
|
605
|
+
target_plugin = None
|
|
606
|
+
for p in get_loaded_plugins():
|
|
607
|
+
if p.name == plugin_id:
|
|
608
|
+
target_plugin = p
|
|
609
|
+
break
|
|
610
|
+
|
|
611
|
+
if not target_plugin:
|
|
612
|
+
raise HTTPException(status_code=404, detail="Plugin not found")
|
|
613
|
+
|
|
614
|
+
# 获取配置元数据 (NoneBot 插件通常通过 metadata.config 导出 Config 类)
|
|
615
|
+
config_schema = {}
|
|
616
|
+
current_config = {}
|
|
617
|
+
|
|
618
|
+
if target_plugin.metadata and target_plugin.metadata.config:
|
|
619
|
+
try:
|
|
620
|
+
config_class = target_plugin.metadata.config
|
|
621
|
+
if hasattr(config_class, "schema"):
|
|
622
|
+
schema = config_class.schema()
|
|
623
|
+
config_schema = schema.get("properties", {})
|
|
624
|
+
# 注入当前值
|
|
625
|
+
driver_config = get_driver().config
|
|
626
|
+
for key in config_schema:
|
|
627
|
+
current_config[key] = getattr(driver_config, key, None)
|
|
628
|
+
except Exception as e:
|
|
629
|
+
logger.error(f"解析插件 {plugin_id} 配置失败: {e}")
|
|
630
|
+
|
|
631
|
+
return {"config": current_config, "schema": config_schema}
|
|
643
632
|
|
|
644
|
-
#
|
|
645
|
-
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
633
|
+
# --- 插件商店相关 API ---
|
|
646
634
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
635
|
+
STORE_URL = "https://registry.nonebot.dev/plugins.json"
|
|
636
|
+
store_cache = {"data": [], "time": 0}
|
|
637
|
+
|
|
638
|
+
@app.get("/web_console/api/store", dependencies=[Depends(check_auth)])
|
|
639
|
+
async def get_store():
|
|
640
|
+
# 缓存 1 小时
|
|
641
|
+
if not store_cache["data"] or time.time() - store_cache["time"] > 3600:
|
|
642
|
+
try:
|
|
643
|
+
async with httpx.AsyncClient(follow_redirects=True, verify=False) as client:
|
|
644
|
+
resp = await client.get(STORE_URL, timeout=15.0)
|
|
645
|
+
if resp.status_code == 200:
|
|
646
|
+
store_cache["data"] = resp.json()
|
|
647
|
+
store_cache["time"] = time.time()
|
|
648
|
+
else:
|
|
649
|
+
logger.error(f"获取 NoneBot 商店数据失败: HTTP {resp.status_code}")
|
|
650
|
+
except Exception as e:
|
|
651
|
+
logger.error(f"获取 NoneBot 商店数据失败: {e}")
|
|
652
|
+
# 如果之前有缓存,即使失败也返回旧缓存,避免页面空白
|
|
653
|
+
if store_cache["data"]:
|
|
654
|
+
return store_cache["data"]
|
|
655
|
+
return {"error": "无法连接到 NoneBot 商店,请检查服务器网络或稍后再试"}
|
|
656
|
+
|
|
657
|
+
return store_cache["data"]
|
|
658
|
+
|
|
659
|
+
@app.post("/web_console/api/store/action", dependencies=[Depends(check_auth)])
|
|
660
|
+
async def store_action(request: Request):
|
|
661
|
+
data = await request.json()
|
|
662
|
+
action = data.get("action") # install, update, uninstall
|
|
663
|
+
plugin_name = data.get("plugin")
|
|
655
664
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
+
if not action or not plugin_name:
|
|
666
|
+
return {"error": "参数错误"}
|
|
667
|
+
|
|
668
|
+
if not re.match(r'^[a-zA-Z0-9_-]+$', plugin_name):
|
|
669
|
+
return {"error": "非法插件名称"}
|
|
670
|
+
|
|
671
|
+
# 执行命令
|
|
672
|
+
import asyncio
|
|
673
|
+
import sys
|
|
665
674
|
|
|
666
|
-
|
|
675
|
+
# 构建命令
|
|
676
|
+
cmd = []
|
|
677
|
+
# 尝试定位 nb 命令
|
|
678
|
+
import shutil
|
|
679
|
+
nb_path = shutil.which("nb")
|
|
667
680
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
return data.decode("utf-8", errors="replace").strip()
|
|
681
|
+
if not nb_path:
|
|
682
|
+
# 如果系统 PATH 中找不到,再尝试在 Python 脚本目录下找
|
|
683
|
+
script_dir = os.path.dirname(sys.executable)
|
|
684
|
+
possible_nb = os.path.join(script_dir, "nb.exe" if sys.platform == "win32" else "nb")
|
|
685
|
+
if os.path.exists(possible_nb):
|
|
686
|
+
nb_path = possible_nb
|
|
687
|
+
else:
|
|
688
|
+
nb_path = "nb" # 最后的保底,尝试直接运行 nb
|
|
677
689
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
690
|
+
# 获取项目根目录 (通常是当前工作目录)
|
|
691
|
+
root_dir = Path.cwd()
|
|
692
|
+
|
|
693
|
+
if action == "install":
|
|
694
|
+
cmd = [nb_path, "plugin", "install", plugin_name]
|
|
695
|
+
elif action == "update":
|
|
696
|
+
cmd = [nb_path, "plugin", "update", plugin_name]
|
|
697
|
+
elif action == "uninstall":
|
|
698
|
+
cmd = [nb_path, "plugin", "uninstall", plugin_name]
|
|
685
699
|
else:
|
|
686
|
-
|
|
687
|
-
logger.error(f"插件操作失败: {error_msg}")
|
|
688
|
-
return {"error": error_msg}
|
|
689
|
-
|
|
690
|
-
except Exception as e:
|
|
691
|
-
logger.error(f"执行插件命令时发生异常: {e}")
|
|
692
|
-
return {"error": str(e)}
|
|
700
|
+
return {"error": "无效操作"}
|
|
693
701
|
|
|
694
|
-
|
|
695
|
-
logger.error(f"执行命令异常: {e}")
|
|
696
|
-
return {"error": str(e)}
|
|
697
|
-
|
|
698
|
-
@app.post("/web_console/api/plugins/{plugin_id}/config", dependencies=[Depends(check_auth)])
|
|
699
|
-
async def update_plugin_config(plugin_id: str, new_config: dict):
|
|
700
|
-
# 这里仅做模拟保存逻辑,实际应用中通常需要修改 .env 文件或数据库
|
|
701
|
-
# 由于直接修改运行中的 config 风险较大,此处返回成功,并提示需要重启
|
|
702
|
-
logger.info(f"收到插件 {plugin_id} 的新配置: {new_config}")
|
|
703
|
-
return {"success": True}
|
|
704
|
-
|
|
705
|
-
# API 路由
|
|
706
|
-
@app.get("/web_console/api/chats", dependencies=[Depends(check_auth)])
|
|
707
|
-
async def get_chats():
|
|
708
|
-
try:
|
|
709
|
-
bot = get_bot()
|
|
710
|
-
if not isinstance(bot, Bot):
|
|
711
|
-
return {"error": "Only OneBot v11 is supported"}
|
|
702
|
+
logger.info(f"开始执行插件操作: {' '.join(cmd)} (工作目录: {root_dir})")
|
|
712
703
|
|
|
713
|
-
|
|
714
|
-
|
|
704
|
+
try:
|
|
705
|
+
process = await asyncio.create_subprocess_exec(
|
|
706
|
+
*cmd,
|
|
707
|
+
stdout=asyncio.subprocess.PIPE,
|
|
708
|
+
stderr=asyncio.subprocess.PIPE,
|
|
709
|
+
cwd=str(root_dir)
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
stdout_bytes, stderr_bytes = await process.communicate()
|
|
713
|
+
|
|
714
|
+
def safe_decode(data: bytes) -> str:
|
|
715
|
+
if not data:
|
|
716
|
+
return ""
|
|
717
|
+
for encoding in ["utf-8", "gbk", "cp936"]:
|
|
718
|
+
try:
|
|
719
|
+
return data.decode(encoding).strip()
|
|
720
|
+
except UnicodeDecodeError:
|
|
721
|
+
continue
|
|
722
|
+
return data.decode("utf-8", errors="replace").strip()
|
|
723
|
+
|
|
724
|
+
stdout = safe_decode(stdout_bytes)
|
|
725
|
+
stderr = safe_decode(stderr_bytes)
|
|
726
|
+
|
|
727
|
+
if process.returncode == 0:
|
|
728
|
+
msg = f"插件 {plugin_name} {action} 成功"
|
|
729
|
+
logger.info(msg)
|
|
730
|
+
return {"msg": msg, "output": stdout}
|
|
731
|
+
else:
|
|
732
|
+
error_msg = stderr or stdout
|
|
733
|
+
logger.error(f"插件操作失败: {error_msg}")
|
|
734
|
+
return {"error": error_msg}
|
|
735
|
+
|
|
736
|
+
except Exception as e:
|
|
737
|
+
logger.error(f"执行插件命令时发生异常: {e}")
|
|
738
|
+
return {"error": str(e)}
|
|
739
|
+
|
|
740
|
+
@app.post("/web_console/api/plugins/{plugin_id}/config", dependencies=[Depends(check_auth)])
|
|
741
|
+
async def update_plugin_config(plugin_id: str, new_config: dict):
|
|
742
|
+
# 尝试更新 .env 文件
|
|
743
|
+
env_path = Path.cwd() / ".env"
|
|
744
|
+
# 简单查找逻辑
|
|
745
|
+
if not env_path.exists():
|
|
746
|
+
for name in [".env.prod", ".env.dev"]:
|
|
747
|
+
p = Path.cwd() / name
|
|
748
|
+
if p.exists():
|
|
749
|
+
env_path = p
|
|
750
|
+
break
|
|
715
751
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
752
|
+
try:
|
|
753
|
+
if env_path.exists():
|
|
754
|
+
content = env_path.read_text(encoding="utf-8")
|
|
755
|
+
lines = content.splitlines()
|
|
756
|
+
new_lines = []
|
|
757
|
+
keys_updated = set()
|
|
758
|
+
|
|
759
|
+
for line in lines:
|
|
760
|
+
line_strip = line.strip()
|
|
761
|
+
if not line_strip or line_strip.startswith("#"):
|
|
762
|
+
new_lines.append(line)
|
|
763
|
+
continue
|
|
764
|
+
|
|
765
|
+
if "=" in line:
|
|
766
|
+
key = line.split("=", 1)[0].strip()
|
|
767
|
+
if key in new_config:
|
|
768
|
+
val = new_config[key]
|
|
769
|
+
if isinstance(val, bool):
|
|
770
|
+
val_str = str(val).lower()
|
|
771
|
+
else:
|
|
772
|
+
val_str = str(val)
|
|
773
|
+
new_lines.append(f"{key}={val_str}")
|
|
774
|
+
keys_updated.add(key)
|
|
775
|
+
else:
|
|
776
|
+
new_lines.append(line)
|
|
777
|
+
else:
|
|
778
|
+
new_lines.append(line)
|
|
779
|
+
|
|
780
|
+
# 追加新配置
|
|
781
|
+
for key, val in new_config.items():
|
|
782
|
+
if key not in keys_updated:
|
|
783
|
+
if isinstance(val, bool):
|
|
784
|
+
val_str = str(val).lower()
|
|
785
|
+
else:
|
|
786
|
+
val_str = str(val)
|
|
787
|
+
new_lines.append(f"{key}={val_str}")
|
|
788
|
+
|
|
789
|
+
env_path.write_text("\n".join(new_lines), encoding="utf-8")
|
|
790
|
+
logger.info(f"已更新配置文件 {env_path}")
|
|
791
|
+
else:
|
|
792
|
+
logger.warning("未找到 .env 文件,无法持久化配置")
|
|
793
|
+
|
|
794
|
+
except Exception as e:
|
|
795
|
+
logger.error(f"保存配置失败: {e}")
|
|
796
|
+
return {"error": str(e)}
|
|
734
797
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
return message_cache.get(chat_id, [])
|
|
798
|
+
logger.info(f"收到插件 {plugin_id} 的新配置: {new_config}")
|
|
799
|
+
return {"success": True, "msg": "配置已保存至 .env (需重启生效)"}
|
|
738
800
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
801
|
+
# API 路由
|
|
802
|
+
@app.get("/web_console/api/chats", dependencies=[Depends(check_auth)])
|
|
803
|
+
async def get_chats():
|
|
804
|
+
try:
|
|
805
|
+
from nonebot import get_bots
|
|
806
|
+
bots = get_bots()
|
|
807
|
+
if not bots:
|
|
808
|
+
return {"error": "No bot connected"}
|
|
809
|
+
bot = list(bots.values())[0]
|
|
810
|
+
|
|
811
|
+
if not isinstance(bot, Bot):
|
|
812
|
+
return {"error": "Only OneBot v11 is supported"}
|
|
813
|
+
|
|
814
|
+
groups = await bot.get_group_list()
|
|
815
|
+
friends = await bot.get_friend_list()
|
|
749
816
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
817
|
+
return {
|
|
818
|
+
"groups": [
|
|
819
|
+
{
|
|
820
|
+
"id": f"group_{g['group_id']}",
|
|
821
|
+
"name": g['group_name'],
|
|
822
|
+
"avatar": f"https://p.qlogo.cn/gh/{g['group_id']}/{g['group_id']}/640"
|
|
823
|
+
} for g in groups
|
|
824
|
+
],
|
|
825
|
+
"private": [
|
|
826
|
+
{
|
|
827
|
+
"id": f"private_{f['user_id']}",
|
|
828
|
+
"name": f['nickname'] or f['remark'] or str(f['user_id']),
|
|
829
|
+
"avatar": f"https://q1.qlogo.cn/g?b=qq&nk={f['user_id']}&s=640"
|
|
830
|
+
} for f in friends
|
|
831
|
+
]
|
|
832
|
+
}
|
|
833
|
+
except Exception as e:
|
|
834
|
+
return {"error": str(e)}
|
|
835
|
+
|
|
836
|
+
@app.get("/web_console/api/history/{chat_id}", dependencies=[Depends(check_auth)])
|
|
837
|
+
async def get_history(chat_id: str):
|
|
838
|
+
return message_cache.get(chat_id, [])
|
|
839
|
+
|
|
840
|
+
@app.get("/web_console/proxy/image", dependencies=[Depends(check_auth)])
|
|
841
|
+
async def proxy_image(url: str):
|
|
842
|
+
url = unquote(url)
|
|
754
843
|
|
|
844
|
+
# 处理 file:// 协议头 (Linux 下常见)
|
|
845
|
+
if url.startswith("file://"):
|
|
846
|
+
url = url.replace("file:///", "/").replace("file://", "")
|
|
847
|
+
# 在 Windows 下剥离开头的斜杠,例如 /C:/Users -> C:/Users
|
|
848
|
+
if os.name == "nt" and url.startswith("/") and ":" in url:
|
|
849
|
+
url = url.lstrip("/")
|
|
850
|
+
|
|
851
|
+
if url.startswith("http"):
|
|
852
|
+
# 尝试从缓存获取
|
|
853
|
+
if url in image_cache:
|
|
854
|
+
return Response(content=image_cache[url]["content"], media_type=image_cache[url]["type"])
|
|
855
|
+
|
|
856
|
+
try:
|
|
857
|
+
async with httpx.AsyncClient() as client:
|
|
858
|
+
resp = await client.get(url, timeout=10.0, follow_redirects=True)
|
|
859
|
+
if resp.status_code == 200:
|
|
860
|
+
content = resp.content
|
|
861
|
+
media_type = resp.headers.get("content-type", "image/jpeg")
|
|
862
|
+
# 写入缓存
|
|
863
|
+
if len(image_cache) >= CACHE_SIZE:
|
|
864
|
+
image_cache.pop(next(iter(image_cache)))
|
|
865
|
+
image_cache[url] = {"content": content, "type": media_type}
|
|
866
|
+
return Response(content=content, media_type=media_type)
|
|
867
|
+
except Exception as e:
|
|
868
|
+
logger.error(f"代理图片下载失败: {e}")
|
|
869
|
+
|
|
870
|
+
# 尝试作为本地路径处理
|
|
755
871
|
try:
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
image_cache.pop(next(iter(image_cache)))
|
|
764
|
-
image_cache[url] = {"content": content, "type": media_type}
|
|
765
|
-
return Response(content=content, media_type=media_type)
|
|
872
|
+
path = Path(url).resolve()
|
|
873
|
+
# 安全检查:只允许访问当前工作目录下的文件
|
|
874
|
+
if not str(path).startswith(str(Path.cwd())):
|
|
875
|
+
return Response(status_code=403)
|
|
876
|
+
|
|
877
|
+
if path.exists() and path.is_file():
|
|
878
|
+
return FileResponse(str(path))
|
|
766
879
|
except Exception as e:
|
|
767
|
-
logger.error(f"
|
|
880
|
+
logger.error(f"本地图片读取失败: {e}")
|
|
768
881
|
|
|
769
|
-
|
|
770
|
-
try:
|
|
771
|
-
path = Path(url)
|
|
772
|
-
if path.exists() and path.is_file():
|
|
773
|
-
return FileResponse(str(path))
|
|
774
|
-
except Exception as e:
|
|
775
|
-
logger.error(f"本地图片读取失败: {e}")
|
|
776
|
-
|
|
777
|
-
return Response(status_code=404)
|
|
882
|
+
return Response(status_code=404)
|
|
778
883
|
|
|
779
|
-
@app.post("/web_console/api/send", dependencies=[Depends(check_auth)])
|
|
780
|
-
async def send_message(data: dict):
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
group_id = int(chat_id.replace("group_", ""))
|
|
791
|
-
await bot.send_group_msg(group_id=group_id, message=content)
|
|
792
|
-
else:
|
|
793
|
-
user_id = int(chat_id.replace("private_", ""))
|
|
794
|
-
await bot.send_private_msg(user_id=user_id, message=content)
|
|
884
|
+
@app.post("/web_console/api/send", dependencies=[Depends(check_auth)])
|
|
885
|
+
async def send_message(data: dict):
|
|
886
|
+
try:
|
|
887
|
+
from nonebot import get_bots
|
|
888
|
+
bots = get_bots()
|
|
889
|
+
if not bots:
|
|
890
|
+
return {"error": "No bot connected"}
|
|
891
|
+
bot = list(bots.values())[0]
|
|
892
|
+
|
|
893
|
+
chat_id = data.get("chat_id")
|
|
894
|
+
content = data.get("content")
|
|
795
895
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
896
|
+
if not chat_id or not content:
|
|
897
|
+
return {"error": "Invalid data"}
|
|
898
|
+
|
|
899
|
+
if chat_id.startswith("group_"):
|
|
900
|
+
group_id = int(chat_id.replace("group_", ""))
|
|
901
|
+
await bot.send_group_msg(group_id=group_id, message=content)
|
|
902
|
+
else:
|
|
903
|
+
user_id = int(chat_id.replace("private_", ""))
|
|
904
|
+
await bot.send_private_msg(user_id=user_id, message=content)
|
|
905
|
+
|
|
906
|
+
# 发送成功后手动添加一条自己的消息到缓存并推送
|
|
907
|
+
my_msg = {
|
|
908
|
+
"id": 0,
|
|
909
|
+
"time": int(time.time()),
|
|
910
|
+
"type": "group" if chat_id.startswith("group_") else "private",
|
|
911
|
+
"sender_id": bot.self_id,
|
|
912
|
+
"sender_name": "我",
|
|
913
|
+
"sender_avatar": f"https://q1.qlogo.cn/g?b=qq&nk={bot.self_id}&s=640",
|
|
914
|
+
"elements": [{"type": "text", "data": content}],
|
|
915
|
+
"content": content,
|
|
916
|
+
"is_self": True
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if chat_id not in message_cache:
|
|
920
|
+
message_cache[chat_id] = []
|
|
921
|
+
message_cache[chat_id].append(my_msg)
|
|
922
|
+
|
|
923
|
+
await broadcast_message({
|
|
924
|
+
"type": "new_message",
|
|
925
|
+
"chat_id": chat_id,
|
|
926
|
+
"data": my_msg
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
return {"status": "ok"}
|
|
930
|
+
except Exception as e:
|
|
931
|
+
return {"error": str(e)}
|
|
932
|
+
|
|
933
|
+
# WebSocket 端点
|
|
934
|
+
@app.websocket("/web_console/ws")
|
|
935
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
936
|
+
token = websocket.query_params.get("token")
|
|
937
|
+
if not token or not auth_manager.verify_token(token):
|
|
938
|
+
await websocket.close(code=1008)
|
|
939
|
+
return
|
|
940
|
+
|
|
941
|
+
await websocket.accept()
|
|
942
|
+
active_connections.add(websocket)
|
|
943
|
+
try:
|
|
944
|
+
while True:
|
|
945
|
+
# 保持连接,接收心跳或其他
|
|
946
|
+
await websocket.receive_text()
|
|
947
|
+
except WebSocketDisconnect:
|
|
836
948
|
active_connections.remove(websocket)
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
@app.get("/web_console")
|
|
842
|
-
async def index():
|
|
843
|
-
return FileResponse(static_path / "index.html")
|
|
949
|
+
except Exception:
|
|
950
|
+
if websocket in active_connections:
|
|
951
|
+
active_connections.remove(websocket)
|