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)