From af0f6c7ec696f940ff77b74da03d2fca67190f34 Mon Sep 17 00:00:00 2001 From: zhilv Date: Sat, 2 May 2026 19:29:18 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(command):=20=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E8=87=AA=E5=8A=A8=E5=9B=9E=E5=A4=8D=E5=88=B0?= =?UTF-8?q?=20QQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 回调服务器可返回 reply 或 messages 字段,插件自动回复到原消息来源 - reply 为纯文本回复,messages 格式同 /webhook 接口 - 支持通过 group_id/user_id 覆盖回复目标 - 无需回复时返回空 JSON 即可 - 更新 README 文档说明回调响应格式 --- README.md | 29 +++++++++++++ handlers/command.py | 102 ++++++++++++++++++++++++++++++++++++++++---- plugin.py | 4 +- 3 files changed, 126 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f902c2e..999e728 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,35 @@ curl -X POST http://localhost:8081/webhook \ | `group_id` | 群号(私聊消息无此字段) | | `message_id` | 消息 ID | +**回调响应格式(自动回复到 QQ):** + +回调服务器返回 JSON,插件会自动将内容回复到原消息来源(群/私聊)。 + +纯文本回复: +```json +{"reply": "收到你的命令了"} +``` + +批量回复(text/image/video 组合发送,file 单独发送): +```json +{ + "messages": [ + {"type": "text", "msg": "处理结果如下"}, + {"type": "image", "url": "https://example.com/result.png"}, + {"type": "file", "url": "report.pdf"} + ] +} +``` + +| 字段 | 说明 | +|---|---| +| `reply` | 纯文本回复(与 `messages` 二选一,`messages` 优先) | +| `messages` | 批量回复数组,格式同 `/webhook` 的 `messages` 字段 | +| `group_id` | 可选,覆盖回复目标群号(默认回复到原群) | +| `user_id` | 可选,覆盖回复目标 QQ 号(默认回复到原发送者) | + +不需要回复时返回 `{}` 或空响应即可。 + ## 项目结构 ``` diff --git a/handlers/command.py b/handlers/command.py index 1c7d943..53ddf9c 100644 --- a/handlers/command.py +++ b/handlers/command.py @@ -1,11 +1,10 @@ -"""命令监听处理器:匹配 #命令名+空格 格式的消息,转发到外部回调 URL。""" +"""命令监听处理器:匹配 #命令名+空格 格式的消息,转发到外部回调 URL 并自动回复。""" import re import aiohttp from ..config import COMMAND_CALLBACK_URL, COMMAND_LENGTH, COMMAND_PREFIX -from ..response import error, ok def build_command_pattern() -> re.Pattern: @@ -31,11 +30,25 @@ def parse_command(raw_message: str) -> dict | None: } -async def send_command_callback(data: dict, logger) -> bool: - """将命令数据 POST 到外部回调 URL。返回是否成功。""" +async def send_command_callback(data: dict, event, api, logger) -> None: + """将命令数据 POST 到外部回调 URL,根据响应自动回复到 QQ。 + + 回调服务器返回格式: + { + "reply": "回复文本", // 纯文本回复 + "messages": [ // 批量回复(可选,优先于 reply) + {"type": "text", "msg": "..."}, + {"type": "image", "url": "..."}, + {"type": "file", "url": "..."}, + {"type": "video", "url": "..."} + ] + } + + 所有字段均为可选,无回复内容时返回空 JSON 即可。 + """ if not COMMAND_CALLBACK_URL: logger.warning("COMMAND_CALLBACK_URL 未配置,跳过命令回调") - return False + return try: async with aiohttp.ClientSession() as session: @@ -50,8 +63,81 @@ async def send_command_callback(data: dict, logger) -> bool: "命令回调失败: status=%d url=%s body=%s", resp.status, COMMAND_CALLBACK_URL, body[:200], ) - return False - return True + return + + # 解析响应,自动回复 + try: + result = await resp.json(content_type=None) + except Exception: + return + + if not isinstance(result, dict): + return + + await _handle_reply(result, event, api, logger) + except Exception as exc: logger.error("命令回调异常: url=%s error=%s", COMMAND_CALLBACK_URL, exc) - return False + + +async def _handle_reply(result: dict, event, api, logger) -> None: + """处理回调响应,自动回复到原消息来源。""" + group_id = result.get("group_id") or (getattr(event.data, "group_id", None)) + user_id = result.get("user_id") or event.data.user_id + messages = result.get("messages") + reply = result.get("reply") + + # 批量回复(使用 post_group_msg / post_private_msg 组合消息段) + if messages and isinstance(messages, list): + text_parts: list[str] = [] + image_url: str | None = None + video_url: str | None = None + file_msgs: list[dict] = [] + + for msg in messages: + msg_type = msg.get("type", "text") + if msg_type == "text": + text_parts.append(msg.get("msg", "")) + elif msg_type == "image": + image_url = msg.get("url") + elif msg_type == "video": + video_url = msg.get("url") + elif msg_type == "file": + file_msgs.append(msg) + + text = "\n".join(text_parts) if text_parts else None + + try: + if group_id: + if text or image_url or video_url: + await api.qq.post_group_msg( + group_id=group_id, text=text, image=image_url, video=video_url, + ) + for fm in file_msgs: + url = fm.get("url", "") + await api.qq.send_group_file( + group_id=group_id, file=url, name=url.split("/")[-1], + ) + else: + if text or image_url or video_url: + await api.qq.post_private_msg( + user_id=user_id, text=text, image=image_url, video=video_url, + ) + for fm in file_msgs: + url = fm.get("url", "") + await api.qq.send_private_file( + user_id=user_id, file=url, name=url.split("/")[-1], + ) + except Exception as exc: + logger.error("命令回复失败: %s", exc) + return + + # 纯文本回复 + if reply and isinstance(reply, str): + try: + if group_id: + await api.qq.send_group_text(group_id=group_id, text=reply) + else: + await api.qq.send_private_text(user_id=user_id, text=reply) + except Exception as exc: + logger.error("命令回复失败: %s", exc) diff --git a/plugin.py b/plugin.py index 979b85f..1f6891e 100644 --- a/plugin.py +++ b/plugin.py @@ -91,7 +91,9 @@ class WebHookPlugin(NcatBotPlugin): "命令监听匹配: command=%s user=%s group=%s", parsed["command"], data["user_id"], data.get("group_id", "-"), ) - asyncio.create_task(send_command_callback(data, self.logger)) + asyncio.create_task( + send_command_callback(data, event, self.api, self.logger) + ) except Exception as exc: self.logger.error("消息处理异常: %s", exc) except asyncio.CancelledError: