mirror of
https://github.com/snowy-yang/xiaonuan.git
synced 2026-10-21 08:04:48 +00:00
OneBot 11 适配器:反向 WS 接入 QQ(NapCat/Lagrange)
- app/onebot.py: /onebot/v11/ws 反向 WS;私聊全回、群聊仅回应 @她 并自动 @ 回去 - CQ 码字符串与消息段数组双格式解析;会话 id ob-<self_id>-p/g-<目标> 与 TUI 隔离 - action echo 配对调用(send_private_msg / send_group_msg),retcode==0 才落历史 - remember_later 的推送(事件确认/关怀/连续天数/身世)自动转成 QQ 消息 - 主动消息路由:push_proactive 优先 TUI、ob- 会话走 QQ,均不在线保持 pending - 主干重构:chat_turn() 供 TUI 与 OneBot 共用;助手消息由传输层确认送达后落库 - 可选 OB_ACCESS_TOKEN 令牌校验;8 个新测试(回复/CQ剥离/群@/通知转换/主动推送/令牌)
This commit is contained in:
1 parent
8bbab7e92b
commit
ea83b5df4b
6 files changed
+412
-80
No files matched your search
@@ -0,0 +1,140 @@
|
||||
"""OneBot 11 适配器测试(TestClient 模拟 OneBot 实现的反向 WS)"""
|
||||
import json
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
from app import onebot
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_conn():
|
||||
onebot.connections.clear()
|
||||
yield
|
||||
onebot.connections.clear()
|
||||
|
||||
|
||||
def _msg_event(user_id=123, message="你好呀", message_type="private",
|
||||
group_id=None, self_id=10000):
|
||||
ev = {"time": 0, "self_id": self_id, "post_type": "message",
|
||||
"message_type": message_type,
|
||||
"sub_type": "friend" if message_type == "private" else "normal",
|
||||
"message_id": 1, "user_id": user_id, "message": message,
|
||||
"raw_message": str(message),
|
||||
"sender": {"user_id": user_id, "nickname": "tester"}}
|
||||
if group_id:
|
||||
ev["group_id"] = group_id
|
||||
return ev
|
||||
|
||||
|
||||
def _drain_action(ws):
|
||||
"""收一个 action 并替 OneBot 实现回一个成功 echo,返回该 action"""
|
||||
action = json.loads(ws.receive_text())
|
||||
ws.send_text(json.dumps({"status": "ok", "retcode": 0, "data": {},
|
||||
"echo": action["echo"]}))
|
||||
return action
|
||||
|
||||
|
||||
def test_private_reply(client, route_sig, fdb):
|
||||
route_sig({})
|
||||
with client.websocket_connect("/onebot/v11/ws") as ws:
|
||||
ws.send_text(json.dumps(_msg_event(message="你好呀")))
|
||||
action = _drain_action(ws)
|
||||
assert action["action"] == "send_private_msg"
|
||||
assert action["params"]["user_id"] == 123
|
||||
assert "回声模式" in action["params"]["message"]
|
||||
assert "你好呀" in action["params"]["message"]
|
||||
# 会话映射注册成功
|
||||
assert "ob-10000-p123" in onebot.connections[10000].targets
|
||||
time.sleep(0.3)
|
||||
# 对话进了历史(user + assistant)
|
||||
assert len(fdb.history) == 2
|
||||
|
||||
|
||||
def test_cq_code_stripped(client, route_sig, fdb):
|
||||
route_sig({})
|
||||
with client.websocket_connect("/onebot/v11/ws") as ws:
|
||||
ws.send_text(json.dumps(
|
||||
_msg_event(message="[CQ:face,id=1]你叫什么名字")))
|
||||
action = _drain_action(ws)
|
||||
assert "你叫什么名字" in action["params"]["message"]
|
||||
|
||||
|
||||
def test_group_requires_at(client, route_sig, fdb):
|
||||
route_sig({})
|
||||
with client.websocket_connect("/onebot/v11/ws") as ws:
|
||||
# 群里没 @ 她 -> 不回应
|
||||
ws.send_text(json.dumps(_msg_event(
|
||||
message_type="group", group_id=777, message="大家好啊")))
|
||||
# 紧跟一条私聊 -> 第一条收到的 action 应该是私聊回复
|
||||
ws.send_text(json.dumps(_msg_event(message="私聊一下")))
|
||||
action = _drain_action(ws)
|
||||
assert action["action"] == "send_private_msg"
|
||||
assert fdb.history[0]["session"] == "ob-10000-p123"
|
||||
|
||||
|
||||
def test_group_at_replies_with_at(client, route_sig, fdb):
|
||||
route_sig({})
|
||||
with client.websocket_connect("/onebot/v11/ws") as ws:
|
||||
ws.send_text(json.dumps(_msg_event(
|
||||
message_type="group", group_id=777,
|
||||
message=[{"type": "at", "data": {"qq": 10000}},
|
||||
{"type": "text", "data": {"text": " 在吗"}}])))
|
||||
action = _drain_action(ws)
|
||||
assert action["action"] == "send_group_msg"
|
||||
assert action["params"]["group_id"] == 777
|
||||
assert action["params"]["message"].startswith("[CQ:at,qq=123]")
|
||||
assert "回声模式" in action["params"]["message"]
|
||||
# 群会话独立于私聊
|
||||
sessions = {h["session"] for h in fdb.history}
|
||||
assert "ob-10000-g777" in sessions
|
||||
|
||||
|
||||
def test_notify_event_becomes_qq_message(client, route_sig, fdb):
|
||||
from datetime import datetime, timedelta
|
||||
route_sig({"events": [{"kind": "alarm",
|
||||
"fire_at": datetime.now() + timedelta(hours=14),
|
||||
"text": "起床", "repeat": "daily"}]})
|
||||
with client.websocket_connect("/onebot/v11/ws") as ws:
|
||||
ws.send_text(json.dumps(_msg_event(message="每天早上7点叫我起床")))
|
||||
a1 = _drain_action(ws) # 回复
|
||||
assert "回声模式" in a1["params"]["message"]
|
||||
a2 = _drain_action(ws) # 事件确认 -> QQ 消息
|
||||
assert a2["action"] == "send_private_msg"
|
||||
assert "我记下了" in a2["params"]["message"]
|
||||
assert "每天都算" in a2["params"]["message"]
|
||||
assert len(fdb.events) == 1
|
||||
|
||||
|
||||
def test_streak_pushed_proactively_via_onebot(client, route_sig, fdb):
|
||||
route_sig({})
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
# 用 FakeDB 直接种下"昨天来过、已连续 4 天" -> 这次该庆祝第 5 天
|
||||
asyncio.run(fdb.set_meta("ob-10000-p123", "last_active", yesterday))
|
||||
asyncio.run(fdb.set_meta("ob-10000-p123", "streak", "4"))
|
||||
with client.websocket_connect("/onebot/v11/ws") as ws:
|
||||
ws.send_text(json.dumps(_msg_event(message="我来了")))
|
||||
actions = [_drain_action(ws), _drain_action(ws)]
|
||||
texts = [a["params"]["message"] for a in actions]
|
||||
assert any("连续陪伴的第 5 天" in t for t in texts), texts
|
||||
assert any("回声模式" in t for t in texts), texts
|
||||
|
||||
|
||||
def test_access_token_enforced(client, route_sig, monkeypatch):
|
||||
monkeypatch.setattr(onebot, "ACCESS_TOKEN", "secret")
|
||||
with pytest.raises(WebSocketDisconnect):
|
||||
with client.websocket_connect("/onebot/v11/ws") as ws:
|
||||
ws.receive_text()
|
||||
with client.websocket_connect("/onebot/v11/ws?access_token=secret") as ws:
|
||||
ws.send_text(json.dumps(_msg_event(message="在吗")))
|
||||
action = _drain_action(ws)
|
||||
assert "回声模式" in action["params"]["message"]
|
||||
|
||||
|
||||
def test_disconnected_target_returns_false(client, route_sig):
|
||||
import asyncio
|
||||
route_sig({})
|
||||
assert asyncio.run(onebot.send_to_session("ob-x-p1", "hi")) is False
|
||||
Reference in new issue
Block a user