Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions actions/v11/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from actions import register_action
from discord.abc import PrivateChannel
import utils.node2image as node2image
import utils.native_forward as native_forward
from discord.channel import CategoryChannel, ForumChannel
from utils.logger import get_logger
import os
Expand Down Expand Up @@ -325,6 +326,11 @@ async def set_group_card(group_id: int, user_id: int, card: str) -> dict:

@register_action("v11")
async def send_group_forward_msg(group_id: int, messages: list) -> dict:
if config["system"].get("use_native_forward", True):
refs = await native_forward.can_native_forward(messages, group_id)
if refs is not None:
return await native_forward.send_native_forward(group_id, refs)
logger.debug("合并转发存在需降级的节点,回退图片方案")
path = node2image.node2image(messages)
return await send_group_msg(
group_id=group_id,
Expand All @@ -336,6 +342,7 @@ async def send_group_forward_msg(group_id: int, messages: list) -> dict:

@register_action("v11")
async def send_private_forward_msg(user_id: int, messages: list) -> dict:
# 私聊转发暂不支持原生 forward(转发到 DM 的可行性尚未实测),始终走图片方案
path = node2image.node2image(messages)
return await send_private_msg(
user_id=user_id,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "onedisc"
version = "1.0.1"
version = "1.0.2"
description = "OneBot implement for Discord"
authors = [
{name = "XiaoDeng3386",email = "1744793737@qq.com"}
Expand Down
138 changes: 138 additions & 0 deletions test_native_forward.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""can_native_forward 判定逻辑单元测试(无需真实 Discord)"""
import asyncio
import sys
from types import SimpleNamespace
from unittest.mock import patch, AsyncMock

import discord

sys.path.insert(0, "/vol2/@apphome/trim.openclaw/data/workspace/onedisc-dev")

from utils import native_forward as nf


def msg(**kw):
base = dict(
id=1001,
type=discord.MessageType.default,
poll=None,
reference=None,
channel=SimpleNamespace(id=2001),
guild=SimpleNamespace(id=111),
)
base.update(kw)
return SimpleNamespace(**base)


class FakeSession:
def __init__(self, record):
self.record = record

async def __aenter__(self):
return self

async def __aexit__(self, *a):
pass

async def get(self, model, id_):
return self.record


def run(coro):
return asyncio.get_event_loop().run_until_complete(coro)


def patch_env(cached=None, db_record=None, target_guild=SimpleNamespace(id=111), api_get=None):
from contextlib import ExitStack

sess = FakeSession(db_record)
stack = ExitStack()
stack.enter_context(patch.object(nf, "client", SimpleNamespace(
cached_messages=cached or [],
get_channel=lambda cid: SimpleNamespace(guild=target_guild),
)))
stack.enter_context(patch.object(nf, "get_session", lambda: sess))
stack.enter_context(patch.object(nf.discord_api, "call", AsyncMock(return_value=api_get)))
return stack


results = []


def check(name, cond):
results.append((name, cond))
print(f"{'✅' if cond else '❌'} {name}")


# --- 1. 内联内容节点 → 整体回退 ---
with patch_env():
r = run(nf.can_native_forward(
[{"type": "node", "data": {"user_id": 1, "nickname": "x", "content": "hi"}}], 3001))
check("内联节点 → None", r is None)

# --- 2. 引用节点 cache/DB 均无记录 → 回退 ---
with patch_env(cached=[], db_record=None):
r = run(nf.can_native_forward([{"type": "node", "data": {"message_id": 999}}], 3001))
check("channel 无法解析 → None", r is None)

# --- 3. cache 命中但类型不可转发(pins_add)→ 回退 ---
with patch_env(cached=[msg(id=1001, type=discord.MessageType.pins_add)]):
r = run(nf.can_native_forward([{"type": "node", "data": {"message_id": 1001}}], 3001))
check("不可转发类型 → None", r is None)

# --- 4. cache 命中、类型可转发 → 返回 refs ---
with patch_env(cached=[msg(id=1001)]):
r = run(nf.can_native_forward([{"type": "node", "data": {"message_id": 1001}}], 3001))
check("正常引用 → refs", r == [{"message_id": 1001, "channel_id": 2001}])

# --- 5. 转发消息本身(reference.type==forward)→ 回退 ---
with patch_env(cached=[msg(id=1001, reference=SimpleNamespace(type=discord.MessageReferenceType.forward))]):
r = run(nf.can_native_forward([{"type": "node", "data": {"message_id": 1001}}], 3001))
check("转发消息不能再转发 → None", r is None)

# --- 6. 数量超阈值 → 回退 ---
with patch.object(nf, "config", {**nf.config, "system": {**nf.config["system"], "native_forward_max_nodes": 1}}):
with patch_env(cached=[msg(id=1001), msg(id=1002)]):
r = run(nf.can_native_forward(
[{"type": "node", "data": {"message_id": 1001}},
{"type": "node", "data": {"message_id": 1002}}], 3001))
check("超阈值 → None", r is None)
# 阈值 1 但只有 1 条 → 放行
r = run(nf.can_native_forward([{"type": "node", "data": {"message_id": 1001}}], 3001))
check("未超阈值 → refs", r == [{"message_id": 1001, "channel_id": 2001}])

# --- 7. 跨服务器(cache 命中 guild 不同)→ 回退 ---
with patch_env(cached=[msg(id=1001, guild=SimpleNamespace(id=999))], target_guild=SimpleNamespace(id=111)):
r = run(nf.can_native_forward([{"type": "node", "data": {"message_id": 1001}}], 3001))
check("跨服务器 → None", r is None)

# --- 8. DB 命中 + REST 预检通过 → refs ---
with patch_env(db_record=SimpleNamespace(channel=2001), api_get={"type": 0, "guild_id": 111}):
r = run(nf.can_native_forward([{"type": "node", "data": {"message_id": 1001}}], 3001))
check("DB+预检通过 → refs", r == [{"message_id": 1001, "channel_id": 2001}])

# --- 9. DB 命中 + 预检失败(不可转发类型 6=pins_add)→ 回退 ---
with patch_env(db_record=SimpleNamespace(channel=2001), api_get={"type": 6, "guild_id": 111}):
r = run(nf.can_native_forward([{"type": "node", "data": {"message_id": 1001}}], 3001))
check("DB+预检类型不可转发 → None", r is None)

# --- 10. DB 命中 + 预检错误响应(消息不存在)→ 回退 ---
with patch_env(db_record=SimpleNamespace(channel=2001), api_get={"code": 10008, "message": "Unknown Message"}):
r = run(nf.can_native_forward([{"type": "node", "data": {"message_id": 1001}}], 3001))
check("DB+预检消息不存在 → None", r is None)

# --- 11. 混合:一条正常 + 一条内联 → 整体回退 ---
with patch_env(cached=[msg(id=1001)]):
r = run(nf.can_native_forward(
[{"type": "node", "data": {"message_id": 1001}},
{"type": "node", "data": {"user_id": 1, "nickname": "x", "content": "hi"}}], 3001))
check("混合节点 → None", r is None)

# --- 12. 非 node 结构 → 回退 ---
with patch_env():
r = run(nf.can_native_forward([{"type": "text", "data": {"text": "hi"}}], 3001))
check("非 node 结构 → None", r is None)

failed = [n for n, c in results if not c]
print(f"\n共 {len(results)} 例,失败 {len(failed)} 例")
sys.exit(1 if failed else 0)
7 changes: 5 additions & 2 deletions utils/discord_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ def __init__(self, data: dict) -> None:

async def call(method: str, path: str, data: dict | None = None, **params) -> dict:
async with httpx.AsyncClient(
proxies=config["system"].get("proxy"), base_url="https://discord.com/api/v10"
# httpx >= 0.28 已将 proxies 参数改名为 proxy
proxy=config["system"].get("proxy"),
base_url="https://discord.com/api/v10",
) as client:
response = await client.request(
method,
path,
data=data,
# Discord API 需要 JSON 编码(httpx 的 data= 会发表单格式)
json=data,
headers={"Authorization": f"Bot {config['account_token']}"},
**params,
)
Expand Down
Loading
Loading