mirror of
https://github.com/snowy-yang/xiaonuan.git
synced 2026-10-21 08:04:48 +00:00
- 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剥离/群@/通知转换/主动推送/令牌)
141 lines
5.7 KiB
Python
141 lines
5.7 KiB
Python
"""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
|