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"
|