-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix(aiocqhttp): expand forward messages into message_str for context #5087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import asyncio | ||
| import itertools | ||
| import json | ||
| import logging | ||
| import time | ||
| import uuid | ||
|
|
@@ -139,6 +140,118 @@ async def convert_message(self, event: Event) -> AstrBotMessage | None: | |
|
|
||
| return abm | ||
|
|
||
| def _extract_forward_text_from_nodes( | ||
| self, | ||
| nodes: list[Any], | ||
| depth: int = 0, | ||
| max_depth: int = 5, | ||
| ) -> str: | ||
| if depth > max_depth or not isinstance(nodes, list): | ||
| return "" | ||
|
|
||
| lines: list[str] = [] | ||
| for node in nodes: | ||
| if not isinstance(node, dict): | ||
| continue | ||
|
|
||
| sender = node.get("sender", {}) if isinstance(node.get("sender"), dict) else {} | ||
| sender_name = ( | ||
| sender.get("nickname") | ||
| or sender.get("card") | ||
| or sender.get("user_id") | ||
| or "未知用户" | ||
| ) | ||
|
|
||
| raw_content = node.get("message") | ||
| if raw_content is None: | ||
| raw_content = node.get("content", []) | ||
|
|
||
| content_chain: list[Any] = [] | ||
| if isinstance(raw_content, list): | ||
| content_chain = raw_content | ||
| elif isinstance(raw_content, str) and raw_content.strip(): | ||
| try: | ||
| parsed = json.loads(raw_content) | ||
| if isinstance(parsed, list): | ||
| content_chain = parsed | ||
| else: | ||
| content_chain = [{"type": "text", "data": {"text": raw_content}}] | ||
| except Exception: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): 吞掉来自 bot.call_action 的所有异常可能会掩盖真实的集成问题。 直接捕获裸露的 建议实现如下: candidates.extend([{"message_id": forward_id}, {"forward_id": forward_id}])
payload: dict[str, Any] | None = None
for params in candidates:
try:
result = await self.bot.call_action("get_forward_msg", **params)
except Exception:
logger.exception(
"Unexpected error while calling get_forward_msg with params=%s",
params,
)
continue
if isinstance(result, dict):
payload = result
break
if not payload:
return ""
nodes = payload.get("messages") or payload.get("nodes") or []
return self._extract_forward_text_from_nodes(nodes)
Original comment in Englishsuggestion (bug_risk): Swallowing all exceptions from bot.call_action may hide real integration issues. Catching bare Suggested implementation: candidates.extend([{"message_id": forward_id}, {"forward_id": forward_id}])
payload: dict[str, Any] | None = None
for params in candidates:
try:
result = await self.bot.call_action("get_forward_msg", **params)
except Exception:
logger.exception(
"Unexpected error while calling get_forward_msg with params=%s",
params,
)
continue
if isinstance(result, dict):
payload = result
break
if not payload:
return ""
nodes = payload.get("messages") or payload.get("nodes") or []
return self._extract_forward_text_from_nodes(nodes)
|
||
| content_chain = [{"type": "text", "data": {"text": raw_content}}] | ||
|
|
||
| text_parts: list[str] = [] | ||
| for seg in content_chain: | ||
| if not isinstance(seg, dict): | ||
| continue | ||
| seg_type = seg.get("type") | ||
| seg_data = seg.get("data", {}) if isinstance(seg.get("data"), dict) else {} | ||
|
|
||
| if seg_type in ("text", "plain"): | ||
| text = seg_data.get("text", "") | ||
| if isinstance(text, str) and text: | ||
| text_parts.append(text) | ||
| elif seg_type == "at": | ||
| qq = seg_data.get("qq") | ||
| if qq: | ||
| text_parts.append(f"@{qq}") | ||
| elif seg_type == "image": | ||
| text_parts.append("[图片]") | ||
| elif seg_type == "face": | ||
| face_id = seg_data.get("id") | ||
| text_parts.append(f"[表情:{face_id}]" if face_id is not None else "[表情]") | ||
| elif seg_type in ("forward", "forward_msg", "nodes"): | ||
| nested = seg_data.get("content") | ||
| if isinstance(nested, list): | ||
| nested_text = self._extract_forward_text_from_nodes( | ||
| nested, | ||
| depth=depth + 1, | ||
| max_depth=max_depth, | ||
| ) | ||
| if nested_text: | ||
| text_parts.append(nested_text) | ||
| else: | ||
| text_parts.append("[转发消息]") | ||
|
|
||
| node_text = "".join(text_parts).strip() | ||
| if node_text: | ||
| lines.append(f"{sender_name}: {node_text}") | ||
|
|
||
| return "\n".join(lines).strip() | ||
|
|
||
| async def _fetch_forward_text(self, forward_id: str) -> str: | ||
| if not forward_id: | ||
| return "" | ||
|
|
||
| candidates: list[dict[str, Any]] = [{"id": forward_id}] | ||
| if str(forward_id).isdigit(): | ||
| candidates.insert(0, {"id": int(forward_id)}) | ||
| candidates.extend([{"message_id": forward_id}, {"forward_id": forward_id}]) | ||
|
|
||
| payload: dict[str, Any] | None = None | ||
| for params in candidates: | ||
| try: | ||
| payload = await self.bot.call_action("get_forward_msg", **params) | ||
| if isinstance(payload, dict): | ||
| break | ||
| except Exception: | ||
| continue | ||
|
|
||
| if not isinstance(payload, dict): | ||
| return "" | ||
|
|
||
| data = payload.get("data", payload) | ||
| if not isinstance(data, dict): | ||
| return "" | ||
|
|
||
| nodes = ( | ||
| data.get("messages") | ||
| or data.get("message") | ||
| or data.get("nodes") | ||
| or data.get("nodeList") | ||
| ) | ||
| text = self._extract_forward_text_from_nodes(nodes if isinstance(nodes, list) else []) | ||
| return text.strip() | ||
|
|
||
| async def _convert_handle_request_event(self, event: Event) -> AstrBotMessage: | ||
| """OneBot V11 请求类事件""" | ||
| abm = AstrBotMessage() | ||
|
|
@@ -393,6 +506,33 @@ async def _convert_handle_message_event( | |
| text = m["data"].get("markdown") or m["data"].get("content", "") | ||
| abm.message.append(Plain(text=text)) | ||
| message_str += text | ||
| elif t in ("forward", "forward_msg"): | ||
| for m in m_group: | ||
| data = m.get("data", {}) if isinstance(m.get("data"), dict) else {} | ||
| if t in ComponentTypes: | ||
| try: | ||
| abm.message.append(ComponentTypes[t](**data)) | ||
| except Exception: | ||
| pass | ||
|
|
||
| fid = data.get("id") or data.get("message_id") or data.get("forward_id") | ||
| if not fid: | ||
| continue | ||
|
|
||
| forward_text = await self._fetch_forward_text(str(fid)) | ||
| if not forward_text: | ||
| # 至少保留占位,避免纯转发被识别为空输入 | ||
| if not message_str.strip(): | ||
| message_str = "[转发消息]" | ||
| else: | ||
| message_str += "\n[转发消息]" | ||
| continue | ||
|
|
||
| if message_str.strip(): | ||
| message_str += "\n" | ||
| # 限制长度,避免超长转发导致上下文爆炸 | ||
| clipped = forward_text[:4000] | ||
| message_str += f"[转发消息]\n{clipped}" | ||
|
Comment on lines
+534
to
+535
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: 直接做硬性长度截断而不给出任何提示,可能会让下游行为变得难以理解。 4000 字符限制可以保护上下文大小,但下游使用方会看到一个看上去“完整”的 建议实现如下: if message_str.strip():
message_str += "\n"
# 限制长度,避免超长转发导致上下文爆炸,并在被截断时追加标记
if isinstance(forward_text, str) and forward_text:
if len(forward_text) > 4000:
clipped = forward_text[:4000] + "... [内容已截断]"
else:
clipped = forward_text
message_str += f"[转发消息]\n{clipped}"
data = m.get("data", {}) if isinstance(m.get("data"), dict) else {}这里假设 Original comment in Englishsuggestion: Clipping forwarded text at a hard limit without indication may confuse downstream behavior. The 4000-character limit protects context size, but downstream consumers see a full-looking Suggested implementation: if message_str.strip():
message_str += "\n"
# 限制长度,避免超长转发导致上下文爆炸,并在被截断时追加标记
if isinstance(forward_text, str) and forward_text:
if len(forward_text) > 4000:
clipped = forward_text[:4000] + "... [内容已截断]"
else:
clipped = forward_text
message_str += f"[转发消息]\n{clipped}"
data = m.get("data", {}) if isinstance(m.get("data"), dict) else {}This edit assumes |
||
| else: | ||
| for m in m_group: | ||
| try: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (complexity): 建议将归一化、分段渲染、转发内容拉取以及 ID/占位符/截断等逻辑拆分成若干小的辅助方法,让新的转发消息处理逻辑更加声明式、易读。
你可以保留现在的功能,但通过把几个职责拆分到 helper 中来降低整体的理解成本。
1. 拆分
_extract_forward_text_from_nodes当前该方法同时处理:
message/content/ JSON / list / str)你可以把归一化 + 单段渲染逻辑抽成几个小 helper:
这样
_extract_forward_text_from_nodes就主要负责“编排”:在保持原有行为的前提下,让每一块逻辑更容易理解和测试。
2. 在
_fetch_forward_text中抽取 helper可以再加两个小 helper,让重试 + 归一化策略更清晰:
使用方式:
3. 简化
"forward"/"forward_msg"分支可以把 ID 解析、占位符拼接和截断逻辑再拆成 helper,这样这个分支的意图就更清晰了:
然后该分支可以写成:
整体功能保持不变,但核心分支大致就读作“解析 ID → 拉取 → 占位符/追加内容”,具体细节隐藏在几个聚焦度更高的 helper 中。
Original comment in English
issue (complexity): Consider extracting the normalization, segment rendering, forward fetching, and ID/placeholder/clipping logic into small helper methods so the new forward-message handling reads more declaratively.
You can keep the new functionality but reduce the cognitive load by splitting a few responsibilities into helpers.
1. Split
_extract_forward_text_from_nodesRight now it handles:
message/content/ JSON / list / str)You can peel off normalization + per-segment logic into small helpers:
Then
_extract_forward_text_from_nodesbecomes mostly orchestration:This keeps all behavior but makes each piece easier to understand and test.
2. Extract helpers in
_fetch_forward_textTwo small helpers make the retry + normalization strategy clearer:
Usage:
3. Simplify the
"forward"/"forward_msg"branchYou can pull out ID resolution, placeholder formatting, and clipping to helpers so the branch reads more declaratively.
Then the branch:
Functionality remains the same, but the core branch now mostly reads as “resolve id → fetch → placeholder/append”, with the detailed logic hidden in small, focused helpers.