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"