ForcePilot/backend/package/yuxi/channels/adapters/qqbot/setup_wizard.py
Kris 552aef767c feat(qqbot): 实现QQ机器人适配器完整功能模块
新增QQ Bot适配器完整代码栈,包含:
1. 基础适配器入口与工具类封装
2. 会话管理、重试队列与流量控制
3. 命令系统与内置指令(ping/help/status等)
4. 富媒体消息处理与格式转换
5. 引用存储与审批管理
6. 凭证备份与会话持久化
7. 健康检查与交互回调系统
2026-05-12 00:48:04 +08:00

212 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)