ForcePilot/backend/package/yuxi/channels/adapters/qqbot/setup_wizard.py

212 lines
6.4 KiB
Python
Raw Normal View History

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)