fix(lark): Ensure Lark private messages are delivered reliably by falling back from rejected open IDs to known chat IDs without duplicating successful sends. (#10018)

* fix(lark): fall back to private chat ID when open ID send fails

Private file sends can fail with error 230101 when addressed by open ID,
even though the same file can be delivered to the private chat ID.

Persist private open ID to chat ID mappings from incoming messages,
scoped by platform and app ID, while preserving existing session IDs.
Keep open ID as the primary recipient and retry each rejected message
once with its known private chat ID, reusing the content and UUID.
Do not resend successful components or retry transport exceptions.

Propagate proactive send and attachment upload failures to callers so
failed deliveries are not reported as successful. Preserve the existing
boolean failure handling for replies.

Refs: #9989

* test(lark): cover private chat fallback and delivery failures

Reproduce rejected open ID file sends and verify that the same uploaded
file is retried once with the private chat ID and original UUID.

Cover successful primary sends, mixed message chains, failed deliveries,
upload failures, group routing, transport timeouts, and reply behavior.
Verify stored routes survive adapter recreation, remain scoped to the
bot, and cannot be overwritten by group messages.

Isolate sender-name tests from persistent route storage and supply the
platform and app identity required by route persistence.

Validation: 26 related tests passed; ruff format and ruff check passed.
Live Feishu API validation remains pending application credentials.

Refs: #9989
This commit is contained in:
NayukiChiba
2026-09-10 22:53:17 +08:00
committed by GitHub
parent 48024574c8
commit 32a751394c
4 changed files with 480 additions and 38 deletions
@@ -25,6 +25,7 @@ from astrbot.api.platform import (
Platform,
PlatformMetadata,
)
from astrbot.core import sp
from astrbot.core.platform.astr_message_event import MessageSesion
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
from astrbot.core.utils.media_utils import MediaResolver
@@ -510,6 +511,7 @@ class LarkPlatformAdapter(Platform):
session: MessageSesion,
message_chain: MessageChain,
) -> None:
fallback_chat_id = None
if session.message_type == MessageType.GROUP_MESSAGE:
id_type = "chat_id"
receive_id = session.session_id
@@ -518,6 +520,15 @@ class LarkPlatformAdapter(Platform):
else:
id_type = "open_id"
receive_id = session.session_id
try:
fallback_chat_id = await sp.get_async(
"lark",
f"{self.meta().id}:{self.appid}",
f"private_chat:{receive_id}",
None,
)
except Exception as exc:
logger.warning("[Lark] Failed to load private chat route: %s", exc)
# 复用 LarkMessageEvent 中的通用发送逻辑
await LarkMessageEvent.send_message_chain(
@@ -525,6 +536,7 @@ class LarkPlatformAdapter(Platform):
self.lark_api,
receive_id=receive_id,
receive_id_type=id_type,
fallback_chat_id=fallback_chat_id,
)
await super().send_by_session(session, message_chain)
@@ -679,6 +691,16 @@ class LarkPlatformAdapter(Platform):
abm.session_id = abm.group_id
else:
abm.session_id = abm.sender.user_id
if message.chat_type == "p2p" and message.chat_id:
try:
await sp.put_async(
"lark",
f"{self.meta().id}:{self.appid}",
f"private_chat:{sender_open_id}",
message.chat_id,
)
except Exception as exc:
logger.warning("[Lark] Failed to save private chat route: %s", exc)
await self.handle_msg(abm)
+127 -38
View File
@@ -196,21 +196,29 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id: str | None = None,
receive_id: str | None = None,
receive_id_type: str | None = None,
fallback_chat_id: str | None = None,
) -> bool:
"""发送飞书 IM 消息的通用辅助函数
"""Send one IM message, retrying rejected private sends by chat ID once.
Args:
lark_client: 飞书客户端
content: 消息内容(JSON字符串)
msg_type: 消息类型(post/file/audio/media等)
reply_message_id: 回复的消息ID(用于回复消息)
receive_id: 接收者ID(用于主动发送)
receive_id_type: 接收者ID类型(用于主动发送)
lark_client: Lark API client.
content: JSON-encoded message content.
msg_type: Lark message type.
reply_message_id: Message to reply to, when sending a reply.
receive_id: Recipient for proactive messages.
receive_id_type: Recipient ID type for proactive messages.
fallback_chat_id: Known private chat to use if open ID is rejected.
Returns:
是否发送成功
Whether the message was delivered.
Raises:
RuntimeError: A proactive send fails, including its fallback attempt.
ValueError: A proactive send has no recipient or recipient ID type.
"""
if lark_client.im is None:
if not reply_message_id:
raise RuntimeError("[Lark] IM API client is not initialized")
logger.error("[Lark] API Client im 模块未初始化")
return False
@@ -236,10 +244,9 @@ class LarkMessageEvent(AstrMessageEvent):
)
if receive_id_type is None or receive_id is None:
logger.error(
"[Lark] 主动发送消息时,receive_id 和 receive_id_type 不能为空",
raise ValueError(
"[Lark] Proactive messages require receive_id and receive_id_type"
)
return False
request = (
CreateMessageRequest.builder()
@@ -255,8 +262,37 @@ class LarkMessageEvent(AstrMessageEvent):
.build()
)
response = await lark_client.im.v1.message.acreate(request)
if (
not response.success()
and receive_id_type == "open_id"
and fallback_chat_id
):
logger.warning(
"[Lark] Open ID send failed (%s): %s; retrying with private chat ID",
response.code,
response.msg,
)
# Retry only this rejected message, preserving its deduplication UUID.
fallback_request = (
CreateMessageRequest.builder()
.receive_id_type("chat_id")
.request_body(
CreateMessageRequestBody.builder()
.receive_id(fallback_chat_id)
.content(content)
.msg_type(msg_type)
.uuid(request.request_body.uuid)
.build()
)
.build()
)
response = await lark_client.im.v1.message.acreate(fallback_request)
if not response.success():
if not reply_message_id:
raise RuntimeError(
f"[Lark] Message send failed ({response.code}): {response.msg}"
)
logger.error(f"[Lark] 发送飞书消息失败({response.code}): {response.msg}")
return False
@@ -487,6 +523,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id: str | None = None,
receive_id: str | None = None,
receive_id_type: str | None = None,
fallback_chat_id: str | None = None,
) -> bool:
if lark_client.cardkit is None:
logger.error("[Lark] API Client cardkit 模块未初始化,无法发送卡片")
@@ -525,6 +562,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id=reply_message_id,
receive_id=receive_id,
receive_id_type=receive_id_type,
fallback_chat_id=fallback_chat_id,
)
@staticmethod
@@ -535,6 +573,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id: str | None = None,
receive_id: str | None = None,
receive_id_type: str | None = None,
fallback_chat_id: str | None = None,
) -> bool:
if not reasoning_content:
return True
@@ -548,6 +587,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id=reply_message_id,
receive_id=receive_id,
receive_id_type=receive_id_type,
fallback_chat_id=fallback_chat_id,
)
@staticmethod
@@ -557,17 +597,24 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id: str | None = None,
receive_id: str | None = None,
receive_id_type: str | None = None,
fallback_chat_id: str | None = None,
) -> None:
"""通用的消息链发送方法
"""Send each message component with an optional private chat fallback.
Args:
message_chain: 要发送的消息链
lark_client: 飞书客户端
reply_message_id: 回复的消息ID(用于回复消息)
receive_id: 接收者ID(用于主动发送)
receive_id_type: 接收者ID类型,如 'open_id', 'chat_id'(用于主动发送)
message_chain: Message components to send.
lark_client: Lark API client.
reply_message_id: Message to reply to, when sending a reply.
receive_id: Recipient for proactive messages.
receive_id_type: Recipient ID type for proactive messages.
fallback_chat_id: Known private chat to use if open ID is rejected.
Raises:
RuntimeError: A proactive message cannot be delivered.
"""
if lark_client.im is None:
if not reply_message_id:
raise RuntimeError("[Lark] IM API client is not initialized")
logger.error("[Lark] API Client im 模块未初始化")
return
@@ -606,6 +653,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id=reply_message_id,
receive_id=receive_id,
receive_id_type=receive_id_type,
fallback_chat_id=fallback_chat_id,
):
return
@@ -640,6 +688,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id=reply_message_id,
receive_id=receive_id,
receive_id_type=receive_id_type,
fallback_chat_id=fallback_chat_id,
)
# 维持组件顺序:遇到折叠面板标记先 flush 当前普通内容并发送卡片
@@ -659,6 +708,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id=reply_message_id,
receive_id=receive_id,
receive_id_type=receive_id_type,
fallback_chat_id=fallback_chat_id,
)
if not success:
buffered_components.append(
@@ -674,17 +724,32 @@ class LarkMessageEvent(AstrMessageEvent):
# 发送附件
for file_comp in file_components:
await LarkMessageEvent._send_file_message(
file_comp, lark_client, reply_message_id, receive_id, receive_id_type
file_comp,
lark_client,
reply_message_id,
receive_id,
receive_id_type,
fallback_chat_id,
)
for audio_comp in audio_components:
await LarkMessageEvent._send_audio_message(
audio_comp, lark_client, reply_message_id, receive_id, receive_id_type
audio_comp,
lark_client,
reply_message_id,
receive_id,
receive_id_type,
fallback_chat_id,
)
for media_comp in media_components:
await LarkMessageEvent._send_media_message(
media_comp, lark_client, reply_message_id, receive_id, receive_id_type
media_comp,
lark_client,
reply_message_id,
receive_id,
receive_id_type,
fallback_chat_id,
)
async def send(self, message: MessageChain) -> None:
@@ -703,21 +768,28 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id: str | None = None,
receive_id: str | None = None,
receive_id_type: str | None = None,
fallback_chat_id: str | None = None,
) -> None:
"""发送文件消息
"""Upload and send a file.
Args:
file_comp: 文件组件
lark_client: 飞书客户端
reply_message_id: 回复的消息ID(用于回复消息)
receive_id: 接收者ID(用于主动发送)
receive_id_type: 接收者ID类型(用于主动发送)
file_comp: File component to upload.
lark_client: Lark API client.
reply_message_id: Message to reply to, when sending a reply.
receive_id: Recipient for proactive messages.
receive_id_type: Recipient ID type for proactive messages.
fallback_chat_id: Known private chat to use if open ID is rejected.
Raises:
RuntimeError: A proactive message cannot be delivered.
"""
file_path = file_comp.file or ""
file_key = await LarkMessageEvent._upload_lark_file(
lark_client, path=file_path, file_type="stream"
)
if not file_key:
if not reply_message_id:
raise RuntimeError("[Lark] Attachment upload failed")
return
content = json.dumps({"file_key": file_key})
@@ -728,6 +800,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id=reply_message_id,
receive_id=receive_id,
receive_id_type=receive_id_type,
fallback_chat_id=fallback_chat_id,
)
@staticmethod
@@ -737,15 +810,20 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id: str | None = None,
receive_id: str | None = None,
receive_id_type: str | None = None,
fallback_chat_id: str | None = None,
) -> None:
"""发送音频消息
"""Upload and send an audio message.
Args:
audio_comp: 音频组件
lark_client: 飞书客户端
reply_message_id: 回复的消息ID(用于回复消息)
receive_id: 接收者ID(用于主动发送)
receive_id_type: 接收者ID类型(用于主动发送)
audio_comp: Audio component to upload.
lark_client: Lark API client.
reply_message_id: Message to reply to, when sending a reply.
receive_id: Recipient for proactive messages.
receive_id_type: Recipient ID type for proactive messages.
fallback_chat_id: Known private chat to use if open ID is rejected.
Raises:
RuntimeError: A proactive message cannot be delivered.
"""
# 获取音频文件路径
try:
@@ -792,6 +870,8 @@ class LarkMessageEvent(AstrMessageEvent):
logger.warning(f"[Lark] 删除转换后的音频文件失败: {e}")
if not file_key:
if not reply_message_id:
raise RuntimeError("[Lark] Attachment upload failed")
return
await LarkMessageEvent._send_im_message(
@@ -801,6 +881,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id=reply_message_id,
receive_id=receive_id,
receive_id_type=receive_id_type,
fallback_chat_id=fallback_chat_id,
)
@staticmethod
@@ -810,15 +891,20 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id: str | None = None,
receive_id: str | None = None,
receive_id_type: str | None = None,
fallback_chat_id: str | None = None,
) -> None:
"""发送视频消息
"""Upload and send a video message.
Args:
media_comp: 视频组件
lark_client: 飞书客户端
reply_message_id: 回复的消息ID(用于回复消息)
receive_id: 接收者ID(用于主动发送)
receive_id_type: 接收者ID类型(用于主动发送)
media_comp: Video component to upload.
lark_client: Lark API client.
reply_message_id: Message to reply to, when sending a reply.
receive_id: Recipient for proactive messages.
receive_id_type: Recipient ID type for proactive messages.
fallback_chat_id: Known private chat to use if open ID is rejected.
Raises:
RuntimeError: A proactive message cannot be delivered.
"""
# 获取视频文件路径
try:
@@ -865,6 +951,8 @@ class LarkMessageEvent(AstrMessageEvent):
logger.warning(f"[Lark] 删除转换后的视频文件失败: {e}")
if not file_key:
if not reply_message_id:
raise RuntimeError("[Lark] Attachment upload failed")
return
await LarkMessageEvent._send_im_message(
@@ -874,6 +962,7 @@ class LarkMessageEvent(AstrMessageEvent):
reply_message_id=reply_message_id,
receive_id=receive_id,
receive_id_type=receive_id_type,
fallback_chat_id=fallback_chat_id,
)
async def react(self, emoji: str) -> None:
+317
View File
@@ -0,0 +1,317 @@
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
import astrbot.api.message_components as Comp
from astrbot.api.event import MessageChain
from astrbot.core.platform.message_session import MessageSession
from astrbot.core.platform.message_type import MessageType
from astrbot.core.platform.platform import Platform
from astrbot.core.platform.sources.lark import lark_adapter
from astrbot.core.platform.sources.lark.lark_adapter import LarkPlatformAdapter
from astrbot.core.platform.sources.lark.lark_event import LarkMessageEvent
@pytest.fixture
def private_chat(monkeypatch):
"""Build a private conversation with mocked storage and IM responses.
Args:
monkeypatch: Fixture used to isolate storage and network operations.
Returns:
Adapter, inbound event, message API mock, and persistent route storage.
"""
routes = {}
preferences = MagicMock()
preferences.put_async = AsyncMock(
side_effect=lambda scope, scope_id, key, value: routes.__setitem__(
(scope, scope_id, key), value
)
)
preferences.get_async = AsyncMock(
side_effect=lambda scope, scope_id, key, default=None: routes.get(
(scope, scope_id, key), default
)
)
monkeypatch.setattr(lark_adapter, "sp", preferences)
monkeypatch.setattr(Platform, "send_by_session", AsyncMock())
monkeypatch.setattr(
LarkMessageEvent, "_upload_lark_file", AsyncMock(return_value="file_test")
)
create = AsyncMock(
return_value=SimpleNamespace(success=lambda: True, code=0, msg="success")
)
adapter = LarkPlatformAdapter.__new__(LarkPlatformAdapter)
adapter.config = {"id": "lark-test"}
adapter.appid = "cli_test"
adapter.bot_open_id = "ou_bot"
adapter.bot_name = "Test bot"
adapter._user_name_cache = {}
adapter.handle_msg = AsyncMock()
adapter.lark_api = SimpleNamespace(
im=SimpleNamespace(v1=SimpleNamespace(message=SimpleNamespace(acreate=create))),
contact=SimpleNamespace(
v3=SimpleNamespace(
user=SimpleNamespace(
aget=AsyncMock(
return_value=SimpleNamespace(
success=lambda: True,
data=SimpleNamespace(
user=SimpleNamespace(name="Test user")
),
)
)
)
)
),
)
event = SimpleNamespace(
event=SimpleNamespace(
sender=SimpleNamespace(
sender_id=SimpleNamespace(open_id="ou_test_user"), sender_type="user"
),
message=SimpleNamespace(
create_time="1700000000000",
chat_type="p2p",
chat_id="oc_test_private",
parent_id=None,
mentions=None,
content='{"text":"hello"}',
message_id="om_test_inbound",
message_type="text",
),
)
)
return adapter, event, create, routes
@pytest.mark.asyncio
async def test_private_file_retries_failed_open_id_with_chat_id(private_chat):
adapter, event, create, _ = private_chat
await adapter.convert_msg(event)
message = adapter.handle_msg.await_args.args[0]
assert message.session_id == "ou_test_user"
assert message.sender.user_id == "ou_test_user"
create.side_effect = [
SimpleNamespace(
success=lambda: False,
code=230101,
msg="Sending messages to users is temporarily unavailable.",
),
SimpleNamespace(success=lambda: True, code=0, msg="success"),
]
await adapter.send_by_session(
MessageSession("lark-test", MessageType.FRIEND_MESSAGE, message.session_id),
MessageChain([Comp.File(file="test.csv", name="test.csv")]),
)
assert create.await_count == 2
first, fallback = [call.args[0] for call in create.await_args_list]
assert (first.receive_id_type, first.request_body.receive_id) == (
"open_id",
"ou_test_user",
)
assert (fallback.receive_id_type, fallback.request_body.receive_id) == (
"chat_id",
"oc_test_private",
)
assert first.request_body.msg_type == fallback.request_body.msg_type == "file"
assert json.loads(fallback.request_body.content) == {"file_key": "file_test"}
assert first.request_body.uuid == fallback.request_body.uuid
LarkMessageEvent._upload_lark_file.assert_awaited_once()
@pytest.mark.asyncio
async def test_successful_open_id_send_does_not_use_fallback(private_chat):
adapter, event, create, _ = private_chat
await adapter.convert_msg(event)
await adapter.send_by_session(
MessageSession("lark-test", MessageType.FRIEND_MESSAGE, "ou_test_user"),
MessageChain([Comp.Plain("hello")]),
)
create.assert_awaited_once()
request = create.await_args.args[0]
assert request.receive_id_type == "open_id"
assert request.request_body.receive_id == "ou_test_user"
@pytest.mark.asyncio
async def test_mixed_chain_retries_only_failed_file(private_chat):
adapter, event, create, _ = private_chat
await adapter.convert_msg(event)
create.side_effect = [
SimpleNamespace(success=lambda: True, code=0, msg="success"),
SimpleNamespace(success=lambda: False, code=230101, msg="Unavailable"),
SimpleNamespace(success=lambda: True, code=0, msg="success"),
]
await adapter.send_by_session(
MessageSession("lark-test", MessageType.FRIEND_MESSAGE, "ou_test_user"),
MessageChain(
[Comp.Plain("hello"), Comp.File(file="test.csv", name="test.csv")]
),
)
requests = [call.args[0] for call in create.await_args_list]
assert [(req.request_body.msg_type, req.receive_id_type) for req in requests] == [
("post", "open_id"),
("file", "open_id"),
("file", "chat_id"),
]
@pytest.mark.asyncio
@pytest.mark.parametrize("known_chat", [False, True])
async def test_failed_delivery_is_not_reported_as_success(private_chat, known_chat):
adapter, event, create, _ = private_chat
if known_chat:
await adapter.convert_msg(event)
create.return_value = SimpleNamespace(
success=lambda: False, code=230101, msg="Unavailable"
)
with pytest.raises(RuntimeError, match="230101"):
await adapter.send_by_session(
MessageSession("lark-test", MessageType.FRIEND_MESSAGE, "ou_test_user"),
MessageChain([Comp.File(file="test.csv", name="test.csv")]),
)
assert create.await_count == (2 if known_chat else 1)
Platform.send_by_session.assert_not_awaited()
@pytest.mark.asyncio
@pytest.mark.parametrize("session_id", ["oc_group", "ou_test_user%oc_group"])
async def test_group_send_keeps_chat_id_routing(private_chat, session_id):
adapter, event, create, _ = private_chat
event.event.message.chat_type = "group"
event.event.message.chat_id = "oc_group"
await adapter.convert_msg(event)
await adapter.send_by_session(
MessageSession("lark-test", MessageType.GROUP_MESSAGE, session_id),
MessageChain([Comp.Plain("hello")]),
)
create.assert_awaited_once()
request = create.await_args.args[0]
assert (request.receive_id_type, request.request_body.receive_id) == (
"chat_id",
"oc_group",
)
lark_adapter.sp.put_async.assert_not_awaited()
@pytest.mark.asyncio
async def test_transport_timeout_does_not_retry_unknown_delivery(private_chat):
adapter, event, create, _ = private_chat
await adapter.convert_msg(event)
create.side_effect = TimeoutError("Unknown delivery status")
with pytest.raises(TimeoutError):
await adapter.send_by_session(
MessageSession("lark-test", MessageType.FRIEND_MESSAGE, "ou_test_user"),
MessageChain([Comp.Plain("hello")]),
)
create.assert_awaited_once()
@pytest.mark.asyncio
async def test_private_route_survives_adapter_recreation_and_is_scoped(private_chat):
adapter, event, create, routes = private_chat
await adapter.convert_msg(event)
assert routes
restarted = LarkPlatformAdapter.__new__(LarkPlatformAdapter)
restarted.config = adapter.config.copy()
restarted.appid = adapter.appid
restarted.lark_api = adapter.lark_api
create.side_effect = [
SimpleNamespace(success=lambda: False, code=230101, msg="Unavailable"),
SimpleNamespace(success=lambda: True, code=0, msg="success"),
]
session = MessageSession("lark-test", MessageType.FRIEND_MESSAGE, "ou_test_user")
await restarted.send_by_session(session, MessageChain([Comp.Plain("hello")]))
assert create.await_args.args[0].request_body.receive_id == "oc_test_private"
restarted.config = {"id": "another-bot"}
restarted.appid = "cli_another_bot"
create.reset_mock(side_effect=True)
create.return_value = SimpleNamespace(
success=lambda: False, code=230101, msg="Unavailable"
)
with pytest.raises(RuntimeError, match="230101"):
await restarted.send_by_session(
MessageSession("another-bot", MessageType.FRIEND_MESSAGE, "ou_test_user"),
MessageChain([Comp.Plain("hello")]),
)
create.assert_awaited_once()
@pytest.mark.asyncio
async def test_file_upload_failure_does_not_report_delivery(private_chat):
adapter, event, create, _ = private_chat
await adapter.convert_msg(event)
LarkMessageEvent._upload_lark_file.return_value = None
with pytest.raises(RuntimeError, match="upload"):
await adapter.send_by_session(
MessageSession("lark-test", MessageType.FRIEND_MESSAGE, "ou_test_user"),
MessageChain([Comp.File(file="test.csv", name="test.csv")]),
)
create.assert_not_awaited()
Platform.send_by_session.assert_not_awaited()
@pytest.mark.asyncio
async def test_group_message_does_not_replace_private_route(private_chat):
adapter, event, create, _ = private_chat
await adapter.convert_msg(event)
event.event.message.chat_type = "group"
event.event.message.chat_id = "oc_group"
await adapter.convert_msg(event)
create.side_effect = [
SimpleNamespace(success=lambda: False, code=230101, msg="Unavailable"),
SimpleNamespace(success=lambda: True, code=0, msg="success"),
]
await adapter.send_by_session(
MessageSession("lark-test", MessageType.FRIEND_MESSAGE, "ou_test_user"),
MessageChain([Comp.Plain("hello")]),
)
assert create.await_args.args[0].request_body.receive_id == "oc_test_private"
@pytest.mark.asyncio
async def test_reply_failure_keeps_boolean_result_without_proactive_fallback(
private_chat,
):
adapter, _, create, _ = private_chat
reply = AsyncMock(
return_value=SimpleNamespace(
success=lambda: False, code=230101, msg="Unavailable"
)
)
adapter.lark_api.im.v1.message.areply = reply
result = await LarkMessageEvent._send_im_message(
adapter.lark_api,
content='{"text":"hello"}',
msg_type="text",
reply_message_id="om_inbound",
receive_id="ou_test_user",
receive_id_type="open_id",
fallback_chat_id="oc_test_private",
)
assert result is False
reply.assert_awaited_once()
create.assert_not_awaited()
+14
View File
@@ -6,6 +6,18 @@ import pytest
from astrbot.core.platform.sources.lark.lark_adapter import LarkPlatformAdapter
@pytest.fixture(autouse=True)
def mock_private_chat_storage(monkeypatch):
"""Keep sender-name tests independent of persistent routing storage.
Args:
monkeypatch: Fixture used to replace the route storage writer.
"""
monkeypatch.setattr(
"astrbot.core.platform.sources.lark.lark_adapter.sp.put_async", AsyncMock()
)
def _private_message_event(message_id: str = "message-1") -> SimpleNamespace:
"""Builds a Lark private-message event.
@@ -45,6 +57,8 @@ def _adapter(user_response: SimpleNamespace) -> LarkPlatformAdapter:
Lark adapter test double.
"""
adapter = LarkPlatformAdapter.__new__(LarkPlatformAdapter)
adapter.config = {"id": "lark-test"}
adapter.appid = "cli_test"
adapter.bot_open_id = "ou_bot"
adapter.bot_name = "AstrBot"
adapter._user_name_cache = {}