feat(weixin_oc): support reply components#7380
feat(weixin_oc): support reply components#7380Sagiri777 wants to merge 3 commits intoAstrBotDevs:masterfrom
Conversation
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py" line_range="1219-1225" />
<code_context>
+ session_id: str,
+ ref_msg: dict[str, Any],
+ ) -> tuple[Reply | None, dict[str, Any]]:
+ metadata: dict[str, Any] = {
+ "is_reply": True,
+ "ref_msg": ref_msg,
+ "reply_kind": "unknown",
+ "quoted_item_type": None,
+ "quoted_text": None,
+ "reply_to": {"matched": False},
+ }
+ message_item = ref_msg.get("message_item")
</code_context>
<issue_to_address>
**issue (bug_risk):** `is_reply` stays True even when no reply component is built.
Because `metadata["is_reply"]` is always set to `True`, callers may mark `abm.is_reply = True` even when `_build_reply_component_from_ref` returns `(None, metadata)` (e.g. for an invalid `message_item`). Please either reset `is_reply` to `False` before such returns, or only set it to `True` once you know a `Reply` (and quoted content) will actually be created.
</issue_to_address>
### Comment 2
<location path="astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py" line_range="1364" />
<code_context>
item_list = cast(list[dict[str, Any]], msg.get("item_list", []))
+ reply_component = None
+ reply_metadata: dict[str, Any] = {
+ "is_reply": False,
+ "ref_msg": None,
</code_context>
<issue_to_address>
**issue (complexity):** Consider introducing a typed reply-metadata dataclass and a small helper for repeated int-config parsing to simplify the new reply-handling and configuration logic.
You can reduce the new complexity without changing behavior by tightening the types and collapsing some of the plumbing into smaller, focused helpers.
### 1. Replace untyped `reply_metadata` dict with a small dataclass
Right now `reply_metadata` is a `dict[str, Any]` built in two places and accessed by string keys:
```python
reply_metadata: dict[str, Any] = {
"is_reply": False,
"ref_msg": None,
"reply_kind": None,
"quoted_item_type": None,
"quoted_text": None,
"reply_to": {"matched": False},
}
...
abm.is_reply = reply_metadata["is_reply"]
abm.ref_msg = reply_metadata["ref_msg"]
...
```
You can make this more readable and safer by introducing a small dataclass with a `.to_dict()` for compatibility where needed:
```python
@dataclass
class WeixinOCReplyMeta:
is_reply: bool = False
ref_msg: dict[str, Any] | None = None
reply_kind: str | None = None
quoted_item_type: int | None = None
quoted_text: str | None = None
reply_to: dict[str, Any] = field(default_factory=lambda: {"matched": False})
def to_dict(self) -> dict[str, Any]:
return {
"is_reply": self.is_reply,
"ref_msg": self.ref_msg,
"reply_kind": self.reply_kind,
"quoted_item_type": self.quoted_item_type,
"quoted_text": self.quoted_text,
"reply_to": self.reply_to,
}
```
Usage in `_build_reply_component_from_ref`:
```python
async def _build_reply_component_from_ref(
self,
*,
session_id: str,
ref_msg: dict[str, Any],
) -> tuple[Reply | None, WeixinOCReplyMeta]:
meta = WeixinOCReplyMeta(is_reply=True, ref_msg=ref_msg)
...
meta.quoted_item_type = quoted_item_type
meta.reply_kind = self._item_type_to_kind(quoted_item_type)
...
if quoted_text:
meta.quoted_text = quoted_text
meta.reply_to = {
"matched": True,
"strategy": "direct-ref-msg",
"matched_kind": meta.reply_kind,
"matched_text": quoted_text,
"confidence": 1.0,
}
...
return reply, meta
```
And in `_handle_inbound_message`:
```python
reply_component: Reply | None = None
reply_meta = WeixinOCReplyMeta()
for item in item_list:
ref_msg = item.get("ref_msg")
if isinstance(ref_msg, dict):
reply_component, reply_meta = await self._build_reply_component_from_ref(
session_id=from_user_id,
ref_msg=ref_msg,
)
break
...
abm.is_reply = reply_meta.is_reply
abm.ref_msg = reply_meta.ref_msg
abm.reply_kind = reply_meta.reply_kind
abm.quoted_item_type = reply_meta.quoted_item_type
abm.quoted_text = reply_meta.quoted_text
abm.reply_to = reply_meta.reply_to
```
This removes repeated string keys, makes fields explicit, and keeps the logic in `_build_reply_component_from_ref` easier to follow.
---
### 2. Factor out repeated int-config parsing into a helper
The three recent-message cache settings follow the same pattern:
```python
self._recent_message_cache_size = max(
1,
int(platform_config.get(
"weixin_oc_recent_message_cache_size",
self.RECENT_MESSAGE_CACHE_SIZE,
)),
)
...
self._recent_session_cache_ttl_s = max(
60,
int(platform_config.get(
"weixin_oc_recent_session_cache_ttl_s",
self.RECENT_SESSION_CACHE_TTL_S,
)),
)
...
self._max_recent_message_sessions = max(
1,
int(platform_config.get(
"weixin_oc_max_recent_message_sessions",
self.MAX_RECENT_MESSAGE_SESSIONS,
)),
)
```
A small helper keeps `__init__` readable and avoids copy-paste logic:
```python
def _get_int_config(
self,
key: str,
default: int,
minimum: int,
) -> int:
try:
value = int(self.platform_config.get(key, default))
except (TypeError, ValueError):
value = default
return max(minimum, value)
```
Then in `__init__`:
```python
self.platform_config = platform_config # if not already stored
self._recent_message_cache_size = self._get_int_config(
"weixin_oc_recent_message_cache_size",
self.RECENT_MESSAGE_CACHE_SIZE,
1,
)
self._recent_session_cache_ttl_s = self._get_int_config(
"weixin_oc_recent_session_cache_ttl_s",
self.RECENT_SESSION_CACHE_TTL_S,
60,
)
self._max_recent_message_sessions = self._get_int_config(
"weixin_oc_max_recent_message_sessions",
self.MAX_RECENT_MESSAGE_SESSIONS,
1,
)
```
This keeps all existing behavior (defaults, minimums, parsing) but reduces noise and duplication around config handling.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f3d2083f2a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Code Review
This pull request implements a message caching and reply matching system for the weixin_oc platform. It introduces WeixinOCRecentMessage and WeixinOCRecentSessionCache dataclasses to store recent messages and adds logic to match replies using either direct references or timestamp-based proximity. The implementation includes cache pruning based on TTL and session limits, along with comprehensive unit tests. Feedback was provided regarding the need for safer integer conversion of message types to prevent potential crashes when processing malformed external data.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1fdb0e4bf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce5741c8ec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| self._cache_recent_message( | ||
| user_id, | ||
| message_id=uuid.uuid4().hex, | ||
| sender_id=sender_id, |
There was a problem hiding this comment.
Gate outbound reply-cache updates on send success
This method writes to _recent_messages and returns True immediately after request_json without checking the business result fields. The Weixin OC API can return HTTP 200 with non-zero ret/errcode (the adapter already treats those as failures in _poll_inbound_updates), so failed sendmessage calls will still be cached as if delivered. In those cases, later timestamp-based reply inference can bind user replies to phantom bot messages and produce incorrect Reply attribution.
Useful? React with 👍 / 👎.
| sender_nickname=from_user_id, | ||
| timestamp=ts, | ||
| timestamp_ms=create_time_ms, | ||
| components=components, |
There was a problem hiding this comment.
Avoid caching reply-wrapped chains as recent payloads
The inbound handler caches components after a Reply component has already been inserted at the front. When later reply resolution falls back to _match_recent_reply (for refs that only provide timestamp/partial metadata), it reuses this cached chain directly, so replies-to-replies can accumulate nested Reply structures and surface bloated or misleading quoted content rather than the original referenced message content.
Useful? React with 👍 / 👎.
根据 Issue #7379 ,添加了根据返回的消息与历史聊天数据构建Reply组件的功能,解决了插件无法在weixin_oc端获得用户引用的消息问题
Modifications / 改动点
改动文件:
修改了
astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py新增了
tests/test_weixin_oc_reply_parsing.py实现功能:
为
weixin_oc添加Reply组件支持,包括:对于文本消息,给出 JSON 返回的引用原文
对于非文本消息,基于 JSON 返回的时间戳推断最可能引用
This is NOT a breaking change. / 这不是一个破坏性变更。
Screenshots or Test Results / 运行截图或测试结果
图片中使用了本人开发的AstrBot插件passonspeaker,用于实现使机器人将被引用的消息转发至另一个会话,这里以用户引用图片、机器人转发至本会话为例。图片中打码部分为本人微信昵称。
Checklist / 检查清单
😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
/ 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。
🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in
requirements.txtandpyproject.toml./ 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到
requirements.txt和pyproject.toml文件相应位置。😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
Summary by Sourcery
Add reply component support for the Weixin OC platform by constructing Reply messages from referenced message data and recent message history, and ensure reply metadata is propagated with inbound messages.
New Features:
Enhancements:
Tests: