nonebot-plugin-shiro-web-console 0.1.7__tar.gz → 0.1.8__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nonebot-plugin-shiro-web-console
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: 一个用于 NoneBot2 的网页控制台插件,支持通过浏览器查看日志和发送消息
5
5
  Project-URL: Homepage, https://github.com/luojisama/nonebot-plugin-shiro-web-console
6
6
  Project-URL: Bug Tracker, https://github.com/luojisama/nonebot-plugin-shiro-web-console/issues
@@ -16,11 +16,11 @@ from collections import deque
16
16
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, Response, Depends, HTTPException
17
17
  from fastapi.staticfiles import StaticFiles
18
18
  from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
19
- from nonebot import get_app, get_bot, get_bots, get_driver, logger, on_message, on_command, require
19
+ from nonebot import get_app, get_bot, get_bots, get_driver, logger, on_message, on_command, require, on_bot_connect
20
20
  import nonebot_plugin_localstore
21
21
  from .config import Config, config
22
22
  from nonebot.permission import SUPERUSER
23
- from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent, PrivateMessageEvent, MessageSegment
23
+ from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent, PrivateMessageEvent, MessageSegment, Message
24
24
  from nonebot.plugin import PluginMetadata
25
25
 
26
26
  START_TIME = time.time()
@@ -35,7 +35,7 @@ __plugin_meta__ = PluginMetadata(
35
35
  supported_adapters={"~onebot.v11"},
36
36
  extra={
37
37
  "author": "luojisama",
38
- "version": "0.1.7",
38
+ "version": "0.1.8",
39
39
  "pypi_test": "nonebot-plugin-shiro-web-console",
40
40
  },
41
41
  )
@@ -316,35 +316,34 @@ async def handle_login_cmd(bot: Bot, event: MessageEvent):
316
316
  first_url = f"http://{public_ips[0]}:{port}/web_console" if public_ips else f"http://127.0.0.1:{port}/web_console"
317
317
  await login_cmd.finish(f"私聊发送失败,请确保您已添加机器人为好友。\n(当前环境访问地址提示:{first_url})")
318
318
 
319
- # 监听所有消息
320
- msg_matcher = on_message(priority=1, block=False)
321
-
322
- @msg_matcher.handle()
323
- async def handle_all_messages(bot: Bot, event: MessageEvent):
324
- chat_id = get_chat_id(event)
325
-
326
- # 尝试通过 get_msg 获取更详细的消息内容(尤其是 NapCat 等框架提供的 URL)
327
- sender_name = event.sender.nickname or str(event.user_id)
328
- try:
329
- msg_details = await bot.get_msg(message_id=event.message_id)
330
- message = msg_details["message"]
331
- # 如果 get_msg 返回了 sender 信息,则优先使用
332
- if "sender" in msg_details:
333
- sender_name = msg_details["sender"].get("nickname") or msg_details["sender"].get("card") or sender_name
334
- except Exception as e:
335
- logger.warning(f"获取消息详情失败: {e},将使用事件自带消息内容")
336
- message = event.get_message()
337
-
338
- # 消息内容解析
319
+ # 辅助函数:解析消息段
320
+ def parse_message_elements(message_segments) -> List[dict]:
339
321
  elements = []
340
- # 消息唯一 ID:使用适配器提供的原始 ID,以便前端去重
341
- msg_id = str(event.message_id)
342
322
 
343
- # 如果 message 是列表(get_msg 返回格式),直接遍历;如果是 Message 对象,也可以遍历
344
- for seg in message:
345
- # 处理 get_msg 返回的字典格式或 MessageSegment 对象
346
- seg_type = seg["type"] if isinstance(seg, dict) else seg.type
347
- seg_data = seg["data"] if isinstance(seg, dict) else seg.data
323
+ # 鲁棒性处理:如果是字符串,尝试转为 Message 对象
324
+ if isinstance(message_segments, str):
325
+ try:
326
+ # Message(str) 会自动解析 CQ 码(如果适配器支持)或作为纯文本
327
+ message_segments = Message(message_segments)
328
+ except Exception:
329
+ # 降级处理
330
+ return [{"type": "text", "data": {"text": message_segments}}]
331
+
332
+ # 如果是 Message 对象,转为 list
333
+ if hasattr(message_segments, "__iter__") and not isinstance(message_segments, (list, tuple)):
334
+ # Message 对象迭代出来是 MessageSegment
335
+ segments = list(message_segments)
336
+ else:
337
+ segments = message_segments
338
+
339
+ for seg in segments:
340
+ # 兼容 dict 和 MessageSegment
341
+ if isinstance(seg, dict):
342
+ seg_type = seg.get("type")
343
+ seg_data = seg.get("data", {})
344
+ else:
345
+ seg_type = seg.type
346
+ seg_data = seg.data
348
347
 
349
348
  if seg_type == "text":
350
349
  elements.append({"type": "text", "data": seg_data.get("text", "")})
@@ -354,32 +353,126 @@ async def handle_all_messages(bot: Bot, event: MessageEvent):
354
353
  # 优先从 get_msg 的数据中获取 url,NapCat 在 Linux 下可能返回 path 或 file 字段
355
354
  raw_url = seg_data.get("url") or seg_data.get("file") or seg_data.get("path") or ""
356
355
 
357
- # 如果是本地文件路径或非 http 开头的,构造代理 URL
358
- if raw_url and not raw_url.startswith("http"):
359
- # 这种情况下可能是 base64 或者本地路径,或者是 CQ 码中的 file 字段
360
- # 我们先尝试直接传给前端,由前端处理或后续通过代理获取
361
- pass
362
-
363
- # 为了确保显示,所有图片都尝试通过代理中转,除非是 base64
356
+ # 代理链接不带 token,由前端动态注入或 check_auth 处理
357
+ final_url = f"/web_console/proxy/image?url={quote(raw_url)}" if raw_url else ""
364
358
  if raw_url.startswith("data:image"):
365
359
  final_url = raw_url
366
- else:
367
- # 代理链接不带 token,由前端动态注入或 check_auth 处理
368
- final_url = f"/web_console/proxy/image?url={quote(raw_url)}" if raw_url else ""
369
-
360
+
370
361
  elements.append({"type": "image", "data": final_url, "raw": raw_url})
371
362
  elif seg_type == "face":
372
363
  face_id = seg_data.get("id")
373
364
  face_url = f"https://s.p.qq.com/pub/get_face?img_type=3&face_id={face_id}"
374
365
  elements.append({"type": "face", "data": face_url, "id": face_id})
375
366
  elif seg_type == "mface":
376
- # 商城表情(Stickers)
377
367
  url = seg_data.get("url")
378
368
  elements.append({"type": "image", "data": url})
379
369
  elif seg_type == "at":
380
370
  elements.append({"type": "at", "data": seg_data.get("qq")})
381
371
  elif seg_type == "reply":
382
372
  elements.append({"type": "reply", "data": seg_data.get("id")})
373
+
374
+ return elements
375
+
376
+ # Hook: 监听 Bot API 调用,捕获发送的消息
377
+ async def on_api_called(bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any):
378
+ if exception:
379
+ return
380
+
381
+ if api in ["send_group_msg", "send_private_msg", "send_msg"]:
382
+ try:
383
+ # Parse data
384
+ message = data.get("message")
385
+ if isinstance(message, str):
386
+ msg_obj = Message(message)
387
+ elif isinstance(message, list):
388
+ # 假设是 list of dicts
389
+ msg_obj = message
390
+ else:
391
+ msg_obj = message
392
+
393
+ elements = parse_message_elements(msg_obj)
394
+
395
+ # Determine chat_id
396
+ chat_id = ""
397
+ if api == "send_group_msg":
398
+ chat_id = f"group_{data.get('group_id')}"
399
+ elif api == "send_private_msg":
400
+ chat_id = f"private_{data.get('user_id')}"
401
+ elif api == "send_msg":
402
+ if data.get("message_type") == "group":
403
+ chat_id = f"group_{data.get('group_id')}"
404
+ else:
405
+ chat_id = f"private_{data.get('user_id')}"
406
+
407
+ if not chat_id:
408
+ return
409
+
410
+ # Construct msg_data
411
+ msg_id = 0
412
+ if isinstance(result, dict):
413
+ msg_id = result.get("message_id", 0)
414
+ elif isinstance(result, int):
415
+ msg_id = result
416
+
417
+ # 获取 content 字符串表示
418
+ content_str = str(message) if not isinstance(message, list) else "[Message]"
419
+
420
+ msg_data = {
421
+ "id": msg_id,
422
+ "chat_id": chat_id,
423
+ "time": int(time.time()),
424
+ "type": "group" if "group" in chat_id else "private",
425
+ "sender_id": bot.self_id,
426
+ "sender_name": "我",
427
+ "sender_avatar": f"https://q1.qlogo.cn/g?b=qq&nk={bot.self_id}&s=640",
428
+ "elements": elements,
429
+ "content": content_str,
430
+ "self_id": bot.self_id,
431
+ "is_self": True
432
+ }
433
+
434
+ # Add to cache and broadcast
435
+ if chat_id not in message_cache:
436
+ message_cache[chat_id] = []
437
+
438
+ message_cache[chat_id].append(msg_data)
439
+ if len(message_cache[chat_id]) > CACHE_SIZE:
440
+ message_cache[chat_id].pop(0)
441
+
442
+ await broadcast_message({
443
+ "type": "new_message",
444
+ "chat_id": chat_id,
445
+ "data": msg_data
446
+ })
447
+ except Exception as e:
448
+ logger.error(f"处理 Bot 发送消息 Hook 失败: {e}")
449
+
450
+ @on_bot_connect
451
+ async def _(bot: Bot):
452
+ if hasattr(bot, "on_called_api"):
453
+ bot.on_called_api(on_api_called)
454
+
455
+ # 监听所有消息
456
+ msg_matcher = on_message(priority=1, block=False)
457
+
458
+ @msg_matcher.handle()
459
+ async def handle_all_messages(bot: Bot, event: MessageEvent):
460
+ chat_id = get_chat_id(event)
461
+
462
+ # 尝试通过 get_msg 获取更详细的消息内容(尤其是 NapCat 等框架提供的 URL)
463
+ sender_name = event.sender.nickname or str(event.user_id)
464
+ try:
465
+ msg_details = await bot.get_msg(message_id=event.message_id)
466
+ message = msg_details["message"]
467
+ # 如果 get_msg 返回了 sender 信息,则优先使用
468
+ if "sender" in msg_details:
469
+ sender_name = msg_details["sender"].get("nickname") or msg_details["sender"].get("card") or sender_name
470
+ except Exception as e:
471
+ logger.warning(f"获取消息详情失败: {e},将使用事件自带消息内容")
472
+ message = event.get_message()
473
+
474
+ # 使用辅助函数解析消息内容
475
+ elements = parse_message_elements(message)
383
476
 
384
477
  msg_data = {
385
478
  "id": event.message_id,
@@ -835,6 +928,49 @@ if app:
835
928
 
836
929
  @app.get("/web_console/api/history/{chat_id}", dependencies=[Depends(check_auth)])
837
930
  async def get_history(chat_id: str):
931
+ # 优先返回缓存
932
+ if chat_id in message_cache and len(message_cache[chat_id]) > 0:
933
+ return message_cache[chat_id]
934
+
935
+ # 尝试从 Bot 获取历史消息 (OneBot v11 get_group_msg_history)
936
+ try:
937
+ from nonebot import get_bots
938
+ bots = get_bots()
939
+ if bots:
940
+ bot = list(bots.values())[0]
941
+ if chat_id.startswith("group_"):
942
+ group_id = int(chat_id.replace("group_", ""))
943
+ # 尝试调用 NapCat/Go-CQHTTP 的 get_group_msg_history
944
+ res = await bot.call_api("get_group_msg_history", group_id=group_id)
945
+ messages = res.get("messages", [])
946
+
947
+ parsed_msgs = []
948
+ for raw in messages:
949
+ # raw: {message_id, time, sender: {...}, message: [...], raw_message: ...}
950
+ sender = raw.get("sender", {})
951
+ sender_id = sender.get("user_id") or 0
952
+ is_self = str(sender_id) == str(bot.self_id)
953
+
954
+ parsed_msgs.append({
955
+ "id": raw.get("message_id"),
956
+ "chat_id": chat_id,
957
+ "time": raw.get("time"),
958
+ "type": "group",
959
+ "sender_id": sender_id,
960
+ "sender_name": sender.get("nickname") or sender.get("card") or str(sender_id),
961
+ "sender_avatar": f"https://q1.qlogo.cn/g?b=qq&nk={sender_id}&s=640",
962
+ "elements": parse_message_elements(raw.get("message", [])),
963
+ "content": raw.get("raw_message", ""),
964
+ "self_id": bot.self_id,
965
+ "is_self": is_self
966
+ })
967
+
968
+ if parsed_msgs:
969
+ message_cache[chat_id] = parsed_msgs[-CACHE_SIZE:]
970
+ return message_cache[chat_id]
971
+ except Exception as e:
972
+ logger.warning(f"获取历史消息失败: {e}")
973
+
838
974
  return message_cache.get(chat_id, [])
839
975
 
840
976
  @app.get("/web_console/proxy/image", dependencies=[Depends(check_auth)])
@@ -903,29 +1039,6 @@ if app:
903
1039
  user_id = int(chat_id.replace("private_", ""))
904
1040
  await bot.send_private_msg(user_id=user_id, message=content)
905
1041
 
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
1042
  return {"status": "ok"}
930
1043
  except Exception as e:
931
1044
  return {"error": str(e)}
@@ -1344,7 +1344,9 @@
1344
1344
  // 初始化 WebSocket
1345
1345
  function initWS() {
1346
1346
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1347
- ws = new WebSocket(`${protocol}//${window.location.host}/web_console/ws`);
1347
+ // 添加 token 参数防止连接被服务端拒绝 (code 1008)
1348
+ const wsUrl = `${protocol}//${window.location.host}/web_console/ws?token=${authToken}`;
1349
+ ws = new WebSocket(wsUrl);
1348
1350
 
1349
1351
  ws.onopen = () => {
1350
1352
  statusEl.textContent = '● 已连接';
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-shiro-web-console"
3
- version = "0.1.7"
3
+ version = "0.1.8"
4
4
  description = "一个用于 NoneBot2 的网页控制台插件,支持通过浏览器查看日志和发送消息"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"