新增QQ Bot适配器完整代码栈,包含: 1. 基础适配器入口与工具类封装 2. 会话管理、重试队列与流量控制 3. 命令系统与内置指令(ping/help/status等) 4. 富媒体消息处理与格式转换 5. 引用存储与审批管理 6. 凭证备份与会话持久化 7. 健康检查与交互回调系统
212 lines
8.0 KiB
Python
212 lines
8.0 KiB
Python
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
import aiohttp
|
||
from pydantic import BaseModel, Field
|
||
|
||
from yuxi.agents.toolkits.registry import tool
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class ChannelApiInput(BaseModel):
|
||
action: str = Field(
|
||
description="操作类型: list_channels(子频道列表), channel_info(频道信息), "
|
||
"create_channel(创建子频道), update_channel(修改子频道), delete_channel(删除子频道), "
|
||
"channel_permissions(权限信息), members(成员列表), announcements(公告列表)",
|
||
)
|
||
guild_id: str = Field(description="频道 ID")
|
||
channel_id: str = Field(default="", description="子频道 ID(操作特定子频道时需要)")
|
||
params: str = Field(
|
||
default="{}",
|
||
description="JSON 格式的额外参数,如创建/修改频道时的 name/type/position 等",
|
||
)
|
||
|
||
|
||
_CHANNEL_API_GUIDE = """
|
||
使用前请确保 QQ Bot 已配置 app_id 和 app_secret。
|
||
|
||
该工具允许 Agent 通过 QQ 开放平台 API 管理频道,包括:
|
||
- 查看子频道列表
|
||
- 创建/修改/删除子频道
|
||
- 查看频道成员
|
||
- 管理公告
|
||
|
||
所有操作自动处理 Token 鉴权,无需手动管理凭证。
|
||
""".strip()
|
||
|
||
|
||
@tool(
|
||
category="qqbot",
|
||
tags=["QQ机器人", "频道管理"],
|
||
display_name="QQ 频道管理",
|
||
config_guide=_CHANNEL_API_GUIDE,
|
||
)
|
||
async def qqbot_channel_api(
|
||
action: str,
|
||
guild_id: str,
|
||
channel_id: str = "",
|
||
params: str = "{}",
|
||
) -> str:
|
||
"""QQ 频道管理 HTTP 代理工具,用于查询和管理 QQ 频道。
|
||
|
||
支持操作类型:
|
||
- list_channels: 获取子频道列表
|
||
- channel_info: 获取子频道详情
|
||
- create_channel: 创建子频道(需要 params 中包含 name, type, position 等)
|
||
- update_channel: 修改子频道(需要 params 中包含要修改的字段)
|
||
- delete_channel: 删除子频道
|
||
- channel_permissions: 获取子频道权限
|
||
- members: 获取频道成员列表
|
||
- announcements: 获取公告列表
|
||
|
||
Args:
|
||
action: 操作类型
|
||
guild_id: 频道 ID
|
||
channel_id: 子频道 ID(操作特定子频道时需要)
|
||
params: JSON 格式的额外参数字符串
|
||
|
||
Returns:
|
||
操作结果描述
|
||
"""
|
||
import json
|
||
import os
|
||
|
||
app_id = os.environ.get("QQBOT_APP_ID", "")
|
||
app_secret = os.environ.get("QQBOT_CLIENT_SECRET", "")
|
||
|
||
if not app_id or not app_secret:
|
||
return "错误:未配置 QQ Bot 凭证。请设置 QQBOT_APP_ID 和 QQBOT_CLIENT_SECRET 环境变量。"
|
||
|
||
parsed_params = {}
|
||
try:
|
||
parsed_params = json.loads(params)
|
||
except json.JSONDecodeError:
|
||
return "错误:params 参数不是有效的 JSON 格式。"
|
||
|
||
token = await _get_access_token(app_id, app_secret)
|
||
if not token:
|
||
return "错误:无法获取 Access Token,请检查 app_id 和 app_secret 是否正确。"
|
||
|
||
api_base = "https://api.sgroup.qq.com"
|
||
|
||
try:
|
||
result = await _execute_channel_action(token, api_base, action, guild_id, channel_id, parsed_params)
|
||
return result
|
||
except Exception as e:
|
||
logger.exception("qqbot_channel_api error: action=%s guild_id=%s", action, guild_id)
|
||
return f"频道 API 调用失败: {e}"
|
||
|
||
|
||
async def _get_access_token(app_id: str, app_secret: str) -> str:
|
||
try:
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.post(
|
||
"https://api.sgroup.qq.com/oauth2/token",
|
||
json={"app_id": app_id, "app_secret": app_secret},
|
||
) as resp:
|
||
if resp.status == 200:
|
||
data = await resp.json()
|
||
return data.get("access_token", "")
|
||
logger.warning("Token API returned status %d", resp.status)
|
||
return ""
|
||
except Exception:
|
||
logger.exception("Failed to get access token")
|
||
return ""
|
||
|
||
|
||
async def _execute_channel_action(
|
||
token: str,
|
||
api_base: str,
|
||
action: str,
|
||
guild_id: str,
|
||
channel_id: str,
|
||
params: dict,
|
||
) -> str:
|
||
import json
|
||
|
||
headers = {
|
||
"Authorization": f"QQBot {token}",
|
||
"Content-Type": "application/json",
|
||
}
|
||
|
||
async with aiohttp.ClientSession() as session:
|
||
if action == "list_channels":
|
||
async with session.get(f"{api_base}/guilds/{guild_id}/channels", headers=headers) as resp:
|
||
if resp.status != 200:
|
||
return f"获取子频道列表失败: HTTP {resp.status}"
|
||
data = await resp.json()
|
||
channels = data if isinstance(data, list) else data.get("channels", data)
|
||
return json.dumps(channels, ensure_ascii=False, indent=2)
|
||
|
||
elif action == "channel_info":
|
||
if not channel_id:
|
||
return "错误:查询子频道信息需要提供 channel_id"
|
||
async with session.get(f"{api_base}/channels/{channel_id}", headers=headers) as resp:
|
||
if resp.status != 200:
|
||
return f"获取子频道信息失败: HTTP {resp.status}"
|
||
data = await resp.json()
|
||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||
|
||
elif action == "create_channel":
|
||
body = {
|
||
"name": params.get("name", "新频道"),
|
||
"type": params.get("type", 0),
|
||
"sub_type": params.get("sub_type", 0),
|
||
"position": params.get("position", 0),
|
||
"parent_id": params.get("parent_id", "0"),
|
||
"private_type": params.get("private_type", 0),
|
||
}
|
||
async with session.post(
|
||
f"{api_base}/guilds/{guild_id}/channels",
|
||
headers=headers,
|
||
json=body,
|
||
) as resp:
|
||
if resp.status not in (200, 201):
|
||
return f"创建子频道失败: HTTP {resp.status}"
|
||
data = await resp.json()
|
||
return f"子频道创建成功: {json.dumps(data, ensure_ascii=False)}"
|
||
|
||
elif action == "update_channel":
|
||
if not channel_id:
|
||
return "错误:修改子频道需要提供 channel_id"
|
||
body = {k: v for k, v in params.items() if v is not None}
|
||
async with session.patch(
|
||
f"{api_base}/channels/{channel_id}",
|
||
headers=headers,
|
||
json=body,
|
||
) as resp:
|
||
if resp.status != 200:
|
||
return f"修改子频道失败: HTTP {resp.status}"
|
||
data = await resp.json()
|
||
return f"子频道修改成功: {json.dumps(data, ensure_ascii=False)}"
|
||
|
||
elif action == "delete_channel":
|
||
if not channel_id:
|
||
return "错误:删除子频道需要提供 channel_id"
|
||
async with session.delete(f"{api_base}/channels/{channel_id}", headers=headers) as resp:
|
||
if resp.status != 200:
|
||
return f"删除子频道失败: HTTP {resp.status}"
|
||
return f"子频道 {channel_id} 已删除"
|
||
|
||
elif action == "members":
|
||
limit = params.get("limit", 100)
|
||
after = params.get("after", "0")
|
||
url = f"{api_base}/guilds/{guild_id}/members?limit={limit}&after={after}"
|
||
async with session.get(url, headers=headers) as resp:
|
||
if resp.status != 200:
|
||
return f"获取成员列表失败: HTTP {resp.status}"
|
||
data = await resp.json()
|
||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||
|
||
elif action == "announcements":
|
||
async with session.get(f"{api_base}/guilds/{guild_id}/announces", headers=headers) as resp:
|
||
if resp.status != 200:
|
||
return f"获取公告列表失败: HTTP {resp.status}"
|
||
data = await resp.json()
|
||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||
|
||
else:
|
||
return f"不支持的操作类型: {action}。支持的操作: list_channels, channel_info, create_channel, update_channel, delete_channel, channel_permissions, members, announcements"
|