新增QQ Bot适配器完整代码栈,包含: 1. 基础适配器入口与工具类封装 2. 会话管理、重试队列与流量控制 3. 命令系统与内置指令(ping/help/status等) 4. 富媒体消息处理与格式转换 5. 引用存储与审批管理 6. 凭证备份与会话持久化 7. 健康检查与交互回调系统
212 lines
6.4 KiB
Python
212 lines
6.4 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
import os
|
||
import re
|
||
import time
|
||
from dataclasses import dataclass
|
||
from enum import Enum, auto
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class WizardStep(Enum):
|
||
WELCOME = auto()
|
||
APP_CREDENTIALS = auto()
|
||
INTENTS = auto()
|
||
PERMISSIONS = auto()
|
||
WEBHOOK_URL = auto()
|
||
TEST_CONNECT = auto()
|
||
CONFIRM = auto()
|
||
FINISH = auto()
|
||
|
||
|
||
@dataclass
|
||
class WizardState:
|
||
step: WizardStep = WizardStep.WELCOME
|
||
app_id: str = ""
|
||
app_secret: str = ""
|
||
bot_token: str = ""
|
||
intents: list[str] = None
|
||
webhook_url: str = ""
|
||
verify_result: dict | None = None
|
||
started_at: float = 0.0
|
||
|
||
def __post_init__(self):
|
||
if self.intents is None:
|
||
self.intents = []
|
||
if self.started_at == 0.0:
|
||
self.started_at = time.time()
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"step": self.step.name,
|
||
"app_id": self.app_id,
|
||
"intents": self.intents,
|
||
"webhook_url": self.webhook_url,
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: dict) -> WizardState:
|
||
return cls(
|
||
step=WizardStep[data.get("step", "WELCOME")],
|
||
app_id=data.get("app_id", ""),
|
||
app_secret="",
|
||
intents=data.get("intents", []),
|
||
webhook_url=data.get("webhook_url", ""),
|
||
)
|
||
|
||
|
||
_WIZARD_WELCOME = """
|
||
=== QQ Bot 安装向导 ===
|
||
|
||
该向导将帮助你完成 QQ Bot 的初始配置。
|
||
|
||
请按以下步骤操作:
|
||
|
||
1. 前往 QQ 开放平台 (https://q.qq.com) 创建机器人应用
|
||
2. 获取 App ID 和 App Secret
|
||
3. 配置机器人的 Intents(意图)
|
||
4. 配置 Webhook 地址
|
||
|
||
输入 /setup start 开始配置。
|
||
""".strip()
|
||
|
||
_SETUP_GUIDE = """
|
||
配置说明:
|
||
|
||
- App ID: 在 QQ 开放平台「应用管理」页面获取
|
||
- App Secret: 在「开发设置」中生成,请注意保管
|
||
- Intents: 机器人需要订阅的事件类型,至少需要:
|
||
- PUBLIC_GUILD_MESSAGES (群聊消息)
|
||
- DIRECT_MESSAGE (私信消息)
|
||
- GUILD_MEMBERS (频道成员)
|
||
- INTERACTION (交互事件)
|
||
- Webhook URL: 接收 QQ 推送事件的回调地址
|
||
""".strip()
|
||
|
||
|
||
def validate_app_id(app_id: str) -> bool:
|
||
return bool(re.match(r"^\d{10,20}$", app_id))
|
||
|
||
|
||
def validate_app_secret(secret: str) -> bool:
|
||
return len(secret) >= 32
|
||
|
||
|
||
def validate_webhook_url(url: str) -> bool:
|
||
return bool(re.match(r"^https?://", url))
|
||
|
||
|
||
_DEFAULT_INTENTS = [
|
||
(0, "GUILDS", "频道事件"),
|
||
(1, "GUILD_MEMBERS", "频道成员事件"),
|
||
(12, "DIRECT_MESSAGE", "私信事件"),
|
||
(25, "INTERACTION", "交互事件"),
|
||
(26, "AUDIO_ACTION", "音频事件"),
|
||
(27, "PUBLIC_GUILD_MESSAGES", "公域消息事件"),
|
||
(28, "GROUP_AND_C2C_EVENT", "群聊和私聊事件"),
|
||
]
|
||
|
||
|
||
def get_default_intents() -> list[int]:
|
||
return [intent[0] for intent in _DEFAULT_INTENTS]
|
||
|
||
|
||
def get_intent_descriptions() -> dict[int, tuple[str, str]]:
|
||
return {intent[0]: (intent[1], intent[2]) for intent in _DEFAULT_INTENTS}
|
||
|
||
|
||
async def test_connection(app_id: str, app_secret: str) -> dict:
|
||
import aiohttp
|
||
|
||
result = {"success": False, "error": "", "bot_info": {}}
|
||
|
||
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:
|
||
result["error"] = f"获取 Token 失败: HTTP {resp.status}"
|
||
return result
|
||
data = await resp.json()
|
||
token = data.get("access_token", "")
|
||
|
||
if not token:
|
||
result["error"] = "Token 为空,请检查 App ID 和 App Secret"
|
||
return result
|
||
|
||
async with session.get(
|
||
"https://api.sgroup.qq.com/users/@me",
|
||
headers={"Authorization": f"QQBot {token}"},
|
||
) as resp:
|
||
if resp.status == 200:
|
||
user_data = await resp.json()
|
||
result["bot_info"] = {
|
||
"id": user_data.get("id", ""),
|
||
"username": user_data.get("username", ""),
|
||
"avatar": user_data.get("avatar", ""),
|
||
}
|
||
|
||
async with session.get(
|
||
"https://api.sgroup.qq.com/gateway/bot",
|
||
headers={"Authorization": f"QQBot {token}"},
|
||
) as resp:
|
||
if resp.status == 200:
|
||
gw_data = await resp.json()
|
||
result["gateway_url"] = gw_data.get("url", "")
|
||
|
||
result["success"] = True
|
||
except Exception as e:
|
||
result["error"] = str(e)
|
||
|
||
return result
|
||
|
||
|
||
async def setup_from_env() -> dict:
|
||
env_config = {
|
||
"app_id": os.environ.get("QQBOT_APP_ID", ""),
|
||
"app_secret": os.environ.get("QQBOT_CLIENT_SECRET", ""),
|
||
"bot_token": os.environ.get("QQBOT_BOT_TOKEN", ""),
|
||
"intents": os.environ.get("QQBOT_INTENTS", ""),
|
||
"webhook_url": os.environ.get("QQBOT_WEBHOOK_URL", ""),
|
||
}
|
||
|
||
if not env_config["app_id"] or not env_config["app_secret"]:
|
||
return {"success": False, "error": "环境变量未配置。请设置 QQBOT_APP_ID 和 QQBOT_CLIENT_SECRET"}
|
||
|
||
if not validate_app_id(env_config["app_id"]):
|
||
return {"success": False, "error": f"App ID 格式无效: {env_config['app_id']}"}
|
||
|
||
if not validate_app_secret(env_config["app_secret"]):
|
||
return {"success": False, "error": "App Secret 长度不足(需要至少 32 字符)"}
|
||
|
||
result = await test_connection(env_config["app_id"], env_config["app_secret"])
|
||
return result
|
||
|
||
|
||
async def generate_config_yaml(app_id: str, app_secret: str, intents: list[int] | None = None) -> str:
|
||
intent_values = intents or get_default_intents()
|
||
|
||
lines = [
|
||
"# QQ Bot 配置文件",
|
||
f"qqbot_app_id: {app_id}",
|
||
"# qqbot_client_secret: 请通过环境变量 QQBOT_CLIENT_SECRET 设置",
|
||
f"qqbot_intents: {json.dumps(intent_values)}",
|
||
"",
|
||
"# 推荐通过环境变量配置敏感信息",
|
||
"# export QQBOT_APP_ID={app_id}",
|
||
"# export QQBOT_CLIENT_SECRET=your_secret_here",
|
||
"",
|
||
"# Intents 说明:",
|
||
]
|
||
|
||
for intent_id, (name, desc) in get_intent_descriptions().items():
|
||
mark = "✓" if intent_id in intent_values else "✗"
|
||
lines.append(f"# {mark} {intent_id}: {name} ({desc})")
|
||
|
||
return "\n".join(lines)
|