This commit is contained in:
2025-11-05 17:27:15 +08:00
parent 2ee9fd3e2b
commit dc1766e023
4 changed files with 58 additions and 384 deletions

View File

@@ -16,6 +16,21 @@ class DatabaseModel(BaseModel):
column_defs: Dict[str, str] = Field(default={}) column_defs: Dict[str, str] = Field(default={})
class PluginInterface(ABC): class PluginInterface(ABC):
plugin_instances: Dict[str, "PluginInterface"] = {}
def callback(self, message: str, chat_id: int, user_id: int) -> str:
'''
继承后重写该方法接受消息并返回消息
返回空字符串代表不进行反馈
Args:
message: 消息内容
chat_id: 会话ID
user_id: 用户ID
Returns:
str: 消息内容
'''
return ""
def execute(self, path:str) -> Optional[APIRouter]: def execute(self, path:str) -> Optional[APIRouter]:
''' '''
继承后是否返回路由决定是否启动该插件 继承后是否返回路由决定是否启动该插件
@@ -55,6 +70,12 @@ class PluginInterface(ABC):
''' '''
pass pass
def register_plugin(self, command: str) -> None:
'''
将插件注册, 使其可以被命令匹配
'''
PluginInterface.plugin_instances[command] = self
def register_db_model(self) -> DatabaseModel: def register_db_model(self) -> DatabaseModel:
''' '''
继承后重写该方法注册数据库模型 继承后重写该方法注册数据库模型

View File

@@ -1,175 +1,65 @@
"""Callback路由处理""" """Callback路由处理"""
import logging from ..Convention.Runtime.GlobalConfig import ProjectConfig
import re
from fastapi import APIRouter, Request from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from core.models import CallbackRequest from ..CoreModules.models import CallbackRequest
from core.database import get_db from ..CoreModules.plugin_interface import PluginInterface
from utils.message import get_message_sender
from utils.parser import CommandParser
from utils.rate_limit import get_rate_limiter
logger = logging.getLogger(__name__) config = ProjectConfig()
ALWAYS_RETURN_OK = config.FindItem("always_return_ok", True) # 返回ok可以避免重试
router = APIRouter() router = APIRouter()
@router.get("/callback") @router.get("/callback")
async def callback_verify(): async def callback_verify():
"""Callback可用性校验 - WPS会发送GET请求验证""" """Callback可用性校验"""
logger.info("收到Callback验证请求") config.Log("Info", "收到Callback验证请求")
return JSONResponse({"result": "ok"}) return JSONResponse({"result": "ok"})
@router.post("/callback") @router.post("/callback")
async def callback_receive(request: Request): async def callback_receive(request: Request):
"""接收WPS Callback消息""" """Callback消息"""
try: try:
# 解析请求数据 # 解析请求数据
data = await request.json() data = await request.json()
logger.info(f"收到消息: chatid={data.get('chatid')}, creator={data.get('creator')}") config.Log("Info", f"完整callback数据: {data}")
logger.info(f"消息内容: {data.get('content')}")
logger.info(f"完整callback数据: {data}")
# 验证请求 # 验证请求
try: try:
callback_data = CallbackRequest(**data) callback_data = CallbackRequest(**data)
except Exception as e: except Exception as e:
logger.error(f"请求数据验证失败: {e}") config.Log("Error", f"请求数据验证失败: {e}")
return JSONResponse({"result": "ok"}) # 仍返回ok以避免重试 if ALWAYS_RETURN_OK:
return JSONResponse({"result": "ok"})
else:
return JSONResponse({"result": "error", "message": str(e)})
# 解析指令 # 解析指令
parse_result = CommandParser.parse(callback_data.content) content = callback_data.content
if not parse_result: command = content.split(" ")[0]
# 不是有效指令,忽略 config.Log("Info", f"识别指令: command={command}")
logger.debug("非有效指令,忽略")
return JSONResponse({"result": "ok"})
game_type, command = parse_result # TODO: 处理指令
logger.info(f"识别指令: game_type={game_type}, command={command}") return await handle_command(command, content, callback_data.chatid, callback_data.creator)
# 检查是否包含 @s 参数(私聊标志)
use_private_url = False
# 使用正则表达式匹配独立的 @s 参数(前后有空格或字符串边界)
if re.search(r'\s+@s\s+|\s+@s$|^@s\s+|^@s$', command):
use_private_url = True
# 从命令中移除 @s 参数,保持其他参数不变
command = re.sub(r'\s+@s(\s+|$)|^@s\s+', ' ', command).strip()
logger.info(f"检测到 @s 参数将优先使用个人URL发送反馈清理后的命令: {command}")
# 检查限流
rate_limiter = get_rate_limiter()
if not rate_limiter.is_allowed():
remaining = rate_limiter.get_remaining()
reset_time = int(rate_limiter.get_reset_time())
sender = get_message_sender()
await sender.send_text(
f"⚠️ 消息发送过于频繁,请等待 {reset_time} 秒后再试\n"
f"剩余配额: {remaining}"
)
return JSONResponse({"result": "ok"})
# 更新用户信息
db = get_db()
db.get_or_create_user(callback_data.creator)
# 处理指令
response_text = await handle_command(
game_type=game_type,
command=command,
chat_id=callback_data.chatid,
user_id=callback_data.creator
)
# 发送回复
if response_text:
# 如果使用了 @s 参数优先发送到个人URL
if use_private_url:
db = get_db()
user_webhook_url = db.get_user_webhook_url(callback_data.creator)
if user_webhook_url:
# 有个人URL发送到个人URL
from utils.message import send_private_message
# 判断消息类型
if game_type == 'ai_chat':
msg_type = 'markdown'
elif response_text.startswith('#'):
msg_type = 'markdown'
else:
msg_type = 'text'
success = await send_private_message(
user_id=callback_data.creator,
content=response_text,
msg_type=msg_type
)
if not success:
# 如果私聊发送失败回退到主URL
logger.warning(f"个人URL发送失败回退到主URL: user_id={callback_data.creator}")
sender = get_message_sender()
if game_type == 'ai_chat':
try:
await sender.send_markdown(response_text)
except Exception as send_md_err:
logger.error(f"发送Markdown消息失败改用文本发送: {send_md_err}")
await sender.send_text(response_text)
else:
if response_text.startswith('#'):
await sender.send_markdown(response_text)
else:
await sender.send_text(response_text)
# 成功发送到个人URL不向主URL发送
else:
# 没有个人URL回退到主URL
logger.info(f"用户 {callback_data.creator} 没有注册个人URL使用主URL发送")
sender = get_message_sender()
if game_type == 'ai_chat':
try:
await sender.send_markdown(response_text)
except Exception as send_md_err:
logger.error(f"发送Markdown消息失败改用文本发送: {send_md_err}")
await sender.send_text(response_text)
else:
if response_text.startswith('#'):
await sender.send_markdown(response_text)
else:
await sender.send_text(response_text)
else:
# 没有 @s 参数正常发送到主URL
sender = get_message_sender()
# AI 对话:统一按 Markdown 发送(按任务决策)
if game_type == 'ai_chat':
try:
await sender.send_markdown(response_text)
except Exception as send_md_err:
logger.error(f"发送Markdown消息失败改用文本发送: {send_md_err}")
await sender.send_text(response_text)
else:
# 其他模块保持原有启发式:以 # 开头视为 Markdown否则文本
if response_text.startswith('#'):
await sender.send_markdown(response_text)
else:
await sender.send_text(response_text)
return JSONResponse({"result": "ok"})
except Exception as e: except Exception as e:
logger.error(f"处理Callback异常: {e}", exc_info=True) config.Log("Error", f"处理Callback异常: {e}", exc_info=True)
# 仍然返回ok避免WPS重试 if ALWAYS_RETURN_OK:
return JSONResponse({"result": "ok"}) return JSONResponse({"result": "ok"})
else:
return JSONResponse({"result": "error", "message": str(e)})
async def handle_command(game_type: str, command: str, async def handle_command(command: str, content: str,
chat_id: int, user_id: int) -> str: chat_id: int, user_id: int) -> str:
"""处理游戏指令 """处理游戏指令
Args: Args:
game_type: 游戏类型 command: 指令
command: 完整指令 content: 消息内容
chat_id: 会话ID chat_id: 会话ID
user_id: 用户ID user_id: 用户ID
@@ -177,252 +67,12 @@ async def handle_command(game_type: str, command: str,
回复文本 回复文本
""" """
try: try:
# 帮助指令 plugin = PluginInterface.plugin_instances.get(command, None)
if game_type == 'help': if plugin:
from games.base import get_help_message return plugin.callback(content, chat_id, user_id)
return get_help_message() else:
return f"❌ 未识别指令: {command}"
# 统计指令
if game_type == 'stats':
from games.base import get_stats_message
return get_stats_message(user_id)
# 注册系统
if game_type == 'register':
return await handle_register_command(command, chat_id, user_id)
# 骰娘游戏
if game_type == 'dice':
from games.dice import DiceGame
game = DiceGame()
return await game.handle(command, chat_id, user_id)
# 石头剪刀布
if game_type == 'rps':
from games.rps import RPSGame
game = RPSGame()
return await game.handle(command, chat_id, user_id)
# 运势占卜
if game_type == 'fortune':
from games.fortune import FortuneGame
game = FortuneGame()
return await game.handle(command, chat_id, user_id)
# 猜数字
if game_type == 'guess':
from games.guess import GuessGame
game = GuessGame()
return await game.handle(command, chat_id, user_id)
# 问答游戏
if game_type == 'quiz':
from games.quiz import QuizGame
game = QuizGame()
return await game.handle(command, chat_id, user_id)
# 成语接龙
if game_type == 'idiom':
from games.idiom import IdiomGame
game = IdiomGame()
return await game.handle(command, chat_id, user_id)
# 五子棋
if game_type == 'gomoku':
from games.gomoku import GomokuGame
game = GomokuGame()
return await game.handle(command, chat_id, user_id)
# 积分系统
if game_type == 'points':
from games.points import PointsGame
game = PointsGame()
return await game.handle(command, chat_id, user_id)
# 炼金系统
if game_type == 'alchemy':
from games.alchemy import AlchemyGame
game = AlchemyGame()
return await game.handle(command, chat_id, user_id)
# 冒险系统
if game_type == 'adventure':
from games.adventure import AdventureGame
game = AdventureGame()
return await game.handle(command, chat_id, user_id)
# 积分赠送系统
if game_type == 'gift':
from games.gift import GiftGame
game = GiftGame()
return await game.handle(command, chat_id, user_id)
# 复述功能
if game_type == 'say':
# 提取参数并原样返回
_, args = CommandParser.extract_command_args(command)
args = args.strip()
if not args:
return "用法:.say 你想让我说的话\n别名:.说 / .复述"
return args
# 私聊功能
if game_type == 'talk':
return await handle_talk_command(command, chat_id, user_id)
# AI对话系统
if game_type == 'ai_chat':
from games.ai_chat import AIChatGame
game = AIChatGame()
return await game.handle(command, chat_id, user_id)
# 赌场系统
if game_type == 'casino':
from games.casino import CasinoGame
game = CasinoGame()
return await game.handle(command, chat_id, user_id)
# 狼人杀系统
if game_type == 'werewolf':
from games.werewolf import WerewolfGame
game = WerewolfGame()
return await game.handle(command, chat_id, user_id)
# 未知游戏类型
logger.warning(f"未知游戏类型: {game_type}")
return "❌ 未知的游戏类型"
except Exception as e: except Exception as e:
logger.error(f"处理游戏指令异常: {e}", exc_info=True) config.Log("Error", f"处理指令异常: {e}", exc_info=True)
return f"❌ 处理指令时出错: {str(e)}" return f"❌ 处理指令时出错: {str(e)}"
async def handle_register_command(command: str, chat_id: int, user_id: int) -> str:
"""处理注册命令
Args:
command: 完整指令 ".register name"".register url <url>"
chat_id: 会话ID
user_id: 用户ID
Returns:
注册结果消息
"""
try:
# 提取参数
_, args = CommandParser.extract_command_args(command)
args = args.strip()
# 验证参数
if not args:
return "❌ 请提供要注册的内容!\n\n正确格式:\n`.register <名称>` - 注册用户名\n`.register url <URL>` - 注册webhook URL\n\n示例:\n`.register 张三`\n`.register url https://example.com/webhook?key=xxx`"
# 检查是否为url子命令
parts = args.split(maxsplit=1)
if parts and parts[0].lower() == 'url':
# 处理URL注册
if len(parts) < 2:
return "❌ 请提供webhook URL\n\n正确格式:`.register url <URL>`\n\n示例:\n`.register url https://example.com/webhook?key=xxx`"
webhook_url = parts[1].strip()
# URL验证
if not webhook_url.startswith(('http://', 'https://')):
return "❌ URL格式无效必须以 http:// 或 https:// 开头。"
# 设置URL
db = get_db()
success = db.set_user_webhook_url(user_id, webhook_url)
if success:
return f"✅ Webhook URL注册成功\n\n**您的个人URL**{webhook_url}\n\n私聊消息将发送到此URL。"
else:
return "❌ 注册失败!请稍后重试。"
else:
# 原有的名称注册逻辑
if len(args) > 20:
return "❌ 名称过长最多支持20个字符。"
# 更新用户名称
db = get_db()
success = db.update_user_name(user_id, args)
if success:
return f"✅ 注册成功!\n\n**您的名称**{args}\n\n之后您可以使用这个名称参与各种游戏和功能。"
else:
return "❌ 注册失败!请稍后重试。"
except Exception as e:
logger.error(f"处理注册指令错误: {e}", exc_info=True)
return f"❌ 处理指令出错: {str(e)}"
async def handle_talk_command(command: str, chat_id: int, user_id: int) -> str:
"""处理私聊命令
Args:
command: 完整指令 ".talk <username> <content>"
chat_id: 会话ID
user_id: 发送者用户ID
Returns:
处理结果消息
"""
try:
# 提取参数
_, args = CommandParser.extract_command_args(command)
args = args.strip()
# 验证参数
if not args:
return "❌ 请提供用户名和消息内容!\n\n正确格式:`.talk <用户名> <消息内容>`\n\n示例:\n`.talk 张三 你好,想和你聊聊`\n`.talk 李四 这是一条私聊消息`"
# 解析username和content第一个单词是username剩余部分是content
parts = args.split(maxsplit=1)
if len(parts) < 2:
return "❌ 请提供用户名和消息内容!\n\n正确格式:`.talk <用户名> <消息内容>`\n\n示例:\n`.talk 张三 你好,想和你聊聊`"
target_username = parts[0].strip()
content = parts[1].strip()
if not target_username:
return "❌ 用户名不能为空!\n\n正确格式:`.talk <用户名> <消息内容>`"
if not content:
return "❌ 消息内容不能为空!\n\n正确格式:`.talk <用户名> <消息内容>`"
# 通过用户名查找目标用户
db = get_db()
target_user = db.get_user_by_name(target_username)
if not target_user:
return f"❌ 找不到用户名为「{target_username}」的用户!\n\n提示:目标用户需要使用 `.register <名称>` 注册用户名。"
target_user_id = target_user['user_id']
# 检查目标用户是否有注册名称(应该有,因为是通过名称找到的)
if not target_user.get('username'):
return f"❌ 用户「{target_username}」尚未注册用户名!"
# 检查目标用户是否有个人webhook URL
if not db.has_webhook_url(target_user_id):
return f"❌ 用户「{target_username}」尚未注册个人webhook URL\n\n提示:目标用户需要使用 `.register url <URL>` 注册个人URL后才能接收私聊消息。"
# 发送私聊消息
from utils.message import send_private_message
success = await send_private_message(
user_id=target_user_id,
content=content,
msg_type='text'
)
if success:
# 私聊消息发送成功不向主URL发送提示消息
return ""
else:
# 发送失败时仍然需要提示用户
return f"❌ 发送私聊消息失败,请稍后重试。"
except Exception as e:
logger.error(f"处理私聊指令错误: {e}", exc_info=True)
return f"❌ 处理指令出错: {str(e)}"

View File

@@ -48,6 +48,7 @@ just because program not running to the place where argument been referenced
- **max_concurrent_requests** default: 100 - **max_concurrent_requests** default: 100
- **database_path** file on [Assets](Assets), default: db.db - **database_path** file on [Assets](Assets), default: db.db
- **plugin_dir** where plugins load, default: Plugins - **plugin_dir** where plugins load, default: Plugins
- **always_return_ok** default: true
## Plugins ## Plugins

2
Utilities/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
"""工具模块"""