mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 16:36:53 +08:00
feat: add keyboard button support for proactive and passive message sending
This commit is contained in:
@@ -100,6 +100,7 @@ def send_proactive_c2c_message(
|
|||||||
openid: str,
|
openid: str,
|
||||||
content: str,
|
content: str,
|
||||||
use_markdown: bool = False,
|
use_markdown: bool = False,
|
||||||
|
keyboard: Optional[dict] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
主动发送 C2C 单聊消息(不需要 msg_id)
|
主动发送 C2C 单聊消息(不需要 msg_id)
|
||||||
@@ -108,11 +109,14 @@ def send_proactive_c2c_message(
|
|||||||
:param openid: 用户 openid
|
:param openid: 用户 openid
|
||||||
:param content: 消息内容
|
:param content: 消息内容
|
||||||
:param use_markdown: 是否使用 Markdown 格式(需机器人开通 Markdown 能力)
|
:param use_markdown: 是否使用 Markdown 格式(需机器人开通 Markdown 能力)
|
||||||
|
:param keyboard: 键盘按钮配置
|
||||||
"""
|
"""
|
||||||
if not content or not content.strip():
|
if not content or not content.strip():
|
||||||
raise ValueError("主动消息内容不能为空")
|
raise ValueError("主动消息内容不能为空")
|
||||||
content = content.strip()
|
content = content.strip()
|
||||||
body = {"markdown": {"content": content}, "msg_type": 2} if use_markdown else {"content": content, "msg_type": 0}
|
body = {"markdown": {"content": content}, "msg_type": 2} if use_markdown else {"content": content, "msg_type": 0}
|
||||||
|
if keyboard:
|
||||||
|
body["keyboard"] = {"content": keyboard}
|
||||||
return _api_request(
|
return _api_request(
|
||||||
access_token, "POST", f"/v2/users/{openid}/messages", body
|
access_token, "POST", f"/v2/users/{openid}/messages", body
|
||||||
)
|
)
|
||||||
@@ -123,6 +127,7 @@ def send_proactive_group_message(
|
|||||||
group_openid: str,
|
group_openid: str,
|
||||||
content: str,
|
content: str,
|
||||||
use_markdown: bool = False,
|
use_markdown: bool = False,
|
||||||
|
keyboard: Optional[dict] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
主动发送群聊消息(不需要 msg_id)
|
主动发送群聊消息(不需要 msg_id)
|
||||||
@@ -131,11 +136,14 @@ def send_proactive_group_message(
|
|||||||
:param group_openid: 群聊 openid
|
:param group_openid: 群聊 openid
|
||||||
:param content: 消息内容
|
:param content: 消息内容
|
||||||
:param use_markdown: 是否使用 Markdown 格式(需机器人开通 Markdown 能力)
|
:param use_markdown: 是否使用 Markdown 格式(需机器人开通 Markdown 能力)
|
||||||
|
:param keyboard: 键盘按钮配置
|
||||||
"""
|
"""
|
||||||
if not content or not content.strip():
|
if not content or not content.strip():
|
||||||
raise ValueError("主动消息内容不能为空")
|
raise ValueError("主动消息内容不能为空")
|
||||||
content = content.strip()
|
content = content.strip()
|
||||||
body = {"markdown": {"content": content}, "msg_type": 2} if use_markdown else {"content": content, "msg_type": 0}
|
body = {"markdown": {"content": content}, "msg_type": 2} if use_markdown else {"content": content, "msg_type": 0}
|
||||||
|
if keyboard:
|
||||||
|
body["keyboard"] = {"content": keyboard}
|
||||||
return _api_request(
|
return _api_request(
|
||||||
access_token, "POST", f"/v2/groups/{group_openid}/messages", body
|
access_token, "POST", f"/v2/groups/{group_openid}/messages", body
|
||||||
)
|
)
|
||||||
@@ -146,11 +154,14 @@ def send_c2c_message(
|
|||||||
openid: str,
|
openid: str,
|
||||||
content: str,
|
content: str,
|
||||||
msg_id: Optional[str] = None,
|
msg_id: Optional[str] = None,
|
||||||
|
keyboard: Optional[dict] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""被动回复 C2C 单聊消息(1 小时内最多 4 次)"""
|
"""被动回复 C2C 单聊消息(1 小时内最多 4 次)"""
|
||||||
body = {"content": content, "msg_type": 0, "msg_seq": 1}
|
body = {"content": content, "msg_type": 0, "msg_seq": 1}
|
||||||
if msg_id:
|
if msg_id:
|
||||||
body["msg_id"] = msg_id
|
body["msg_id"] = msg_id
|
||||||
|
if keyboard:
|
||||||
|
body["keyboard"] = {"content": keyboard}
|
||||||
return _api_request(
|
return _api_request(
|
||||||
access_token, "POST", f"/v2/users/{openid}/messages", body
|
access_token, "POST", f"/v2/users/{openid}/messages", body
|
||||||
)
|
)
|
||||||
@@ -161,11 +172,14 @@ def send_group_message(
|
|||||||
group_openid: str,
|
group_openid: str,
|
||||||
content: str,
|
content: str,
|
||||||
msg_id: Optional[str] = None,
|
msg_id: Optional[str] = None,
|
||||||
|
keyboard: Optional[dict] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""被动回复群聊消息(1 小时内最多 4 次)"""
|
"""被动回复群聊消息(1 小时内最多 4 次)"""
|
||||||
body = {"content": content, "msg_type": 0, "msg_seq": 1}
|
body = {"content": content, "msg_type": 0, "msg_seq": 1}
|
||||||
if msg_id:
|
if msg_id:
|
||||||
body["msg_id"] = msg_id
|
body["msg_id"] = msg_id
|
||||||
|
if keyboard:
|
||||||
|
body["keyboard"] = {"content": keyboard}
|
||||||
return _api_request(
|
return _api_request(
|
||||||
access_token, "POST", f"/v2/groups/{group_openid}/messages", body
|
access_token, "POST", f"/v2/groups/{group_openid}/messages", body
|
||||||
)
|
)
|
||||||
@@ -188,6 +202,7 @@ def send_message(
|
|||||||
content: str,
|
content: str,
|
||||||
msg_type: Literal["c2c", "group"] = "c2c",
|
msg_type: Literal["c2c", "group"] = "c2c",
|
||||||
msg_id: Optional[str] = None,
|
msg_id: Optional[str] = None,
|
||||||
|
keyboard: Optional[dict] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
统一发送接口
|
统一发送接口
|
||||||
@@ -196,11 +211,12 @@ def send_message(
|
|||||||
:param content: 消息内容
|
:param content: 消息内容
|
||||||
:param msg_type: c2c 单聊 / group 群聊
|
:param msg_type: c2c 单聊 / group 群聊
|
||||||
:param msg_id: 可选,被动回复时传入原消息 id
|
:param msg_id: 可选,被动回复时传入原消息 id
|
||||||
|
:param keyboard: 可选,键盘按钮配置
|
||||||
"""
|
"""
|
||||||
if msg_id:
|
if msg_id:
|
||||||
if msg_type == "c2c":
|
if msg_type == "c2c":
|
||||||
return send_c2c_message(access_token, target, content, msg_id)
|
return send_c2c_message(access_token, target, content, msg_id, keyboard)
|
||||||
return send_group_message(access_token, target, content, msg_id)
|
return send_group_message(access_token, target, content, msg_id, keyboard)
|
||||||
if msg_type == "c2c":
|
if msg_type == "c2c":
|
||||||
return send_proactive_c2c_message(access_token, target, content)
|
return send_proactive_c2c_message(access_token, target, content, keyboard=keyboard)
|
||||||
return send_proactive_group_message(access_token, target, content)
|
return send_proactive_group_message(access_token, target, content, keyboard=keyboard)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ QQ Bot Gateway WebSocket 客户端
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Callable, List, Optional
|
from typing import Callable, List, Optional
|
||||||
@@ -108,6 +109,9 @@ def run_gateway(
|
|||||||
author = d.get("author", {})
|
author = d.get("author", {})
|
||||||
user_openid = author.get("user_openid", "")
|
user_openid = author.get("user_openid", "")
|
||||||
content = d.get("content", "").strip()
|
content = d.get("content", "").strip()
|
||||||
|
match = re.search(r'(agent_interaction:choice:[\w\-]+:\d+|agent_choice:[\w\-]+:\d+)', content)
|
||||||
|
if match:
|
||||||
|
content = f"CALLBACK:{match.group(1)}"
|
||||||
msg_id = d.get("id", "")
|
msg_id = d.get("id", "")
|
||||||
if content:
|
if content:
|
||||||
on_message_fn({
|
on_message_fn({
|
||||||
@@ -122,6 +126,9 @@ def run_gateway(
|
|||||||
member_openid = author.get("member_openid", "")
|
member_openid = author.get("member_openid", "")
|
||||||
group_openid = d.get("group_openid", "")
|
group_openid = d.get("group_openid", "")
|
||||||
content = d.get("content", "").strip()
|
content = d.get("content", "").strip()
|
||||||
|
match = re.search(r'(agent_interaction:choice:[\w\-]+:\d+|agent_choice:[\w\-]+:\d+)', content)
|
||||||
|
if match:
|
||||||
|
content = f"CALLBACK:{match.group(1)}"
|
||||||
msg_id = d.get("id", "")
|
msg_id = d.get("id", "")
|
||||||
if content:
|
if content:
|
||||||
on_message_fn({
|
on_message_fn({
|
||||||
|
|||||||
@@ -347,13 +347,43 @@ class QQBot:
|
|||||||
logger.warn("QQ Bot: 消息内容为空")
|
logger.warn("QQ Bot: 消息内容为空")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 处理按钮
|
||||||
|
buttons = kwargs.get("buttons")
|
||||||
|
keyboard = None
|
||||||
|
if buttons:
|
||||||
|
rows = []
|
||||||
|
btn_id = 1
|
||||||
|
for row in buttons:
|
||||||
|
btns = []
|
||||||
|
for btn in row:
|
||||||
|
action_type = 0 if btn.get("url") else 2
|
||||||
|
btns.append({
|
||||||
|
"id": str(btn_id),
|
||||||
|
"render_data": {
|
||||||
|
"label": btn.get("text", "按钮")[:30],
|
||||||
|
"visited_label": btn.get("text", "按钮")[:30],
|
||||||
|
"style": 1
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": action_type,
|
||||||
|
"data": btn.get("url") if action_type == 0 else btn.get("callback_data", ""),
|
||||||
|
"permission": {"type": 2}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
btn_id += 1
|
||||||
|
if btns:
|
||||||
|
rows.append({"buttons": btns})
|
||||||
|
if rows:
|
||||||
|
keyboard = {"rows": rows}
|
||||||
|
use_markdown = True
|
||||||
|
|
||||||
success_count = 0
|
success_count = 0
|
||||||
try:
|
try:
|
||||||
token = get_access_token(self._app_id, self._app_secret)
|
token = get_access_token(self._app_id, self._app_secret)
|
||||||
for tgt, tgt_is_group in targets_to_send:
|
for tgt, tgt_is_group in targets_to_send:
|
||||||
send_fn = send_proactive_group_message if tgt_is_group else send_proactive_c2c_message
|
send_fn = send_proactive_group_message if tgt_is_group else send_proactive_c2c_message
|
||||||
try:
|
try:
|
||||||
send_fn(token, tgt, content, use_markdown=use_markdown)
|
send_fn(token, tgt, content, use_markdown=use_markdown, keyboard=keyboard)
|
||||||
success_count += 1
|
success_count += 1
|
||||||
logger.debug(f"QQ Bot: 消息已发送到 {'群' if tgt_is_group else '用户'} {tgt}")
|
logger.debug(f"QQ Bot: 消息已发送到 {'群' if tgt_is_group else '用户'} {tgt}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -371,7 +401,7 @@ class QQBot:
|
|||||||
plain_parts.append(link)
|
plain_parts.append(link)
|
||||||
plain_content = "\n".join(plain_parts).strip()
|
plain_content = "\n".join(plain_parts).strip()
|
||||||
if plain_content:
|
if plain_content:
|
||||||
send_fn(token, tgt, plain_content, use_markdown=False)
|
send_fn(token, tgt, plain_content, use_markdown=False, keyboard=None)
|
||||||
success_count += 1
|
success_count += 1
|
||||||
logger.debug(f"QQ Bot: Markdown 不可用,已回退纯文本发送至 {tgt}")
|
logger.debug(f"QQ Bot: Markdown 不可用,已回退纯文本发送至 {tgt}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -450,7 +450,12 @@ class ChannelCapabilityManager:
|
|||||||
ChannelCapability.RICH_TEXT,
|
ChannelCapability.RICH_TEXT,
|
||||||
ChannelCapability.IMAGES,
|
ChannelCapability.IMAGES,
|
||||||
ChannelCapability.LINKS,
|
ChannelCapability.LINKS,
|
||||||
|
ChannelCapability.INLINE_BUTTONS,
|
||||||
|
ChannelCapability.CALLBACK_QUERIES,
|
||||||
},
|
},
|
||||||
|
max_buttons_per_row=5,
|
||||||
|
max_button_rows=5,
|
||||||
|
max_button_text_length=30,
|
||||||
fallback_enabled=True,
|
fallback_enabled=True,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user