ForcePilot/backend/package/yuxi/channels/adapters/qqbot/tools/channel.py

212 lines
8.0 KiB
Python
Raw Normal View History

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"