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