首次提交
141
.gitignore
vendored
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
pytestdebug.log
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
doc/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||||
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"python.languageServer": "None",
|
||||||
|
"python.analysis.typeCheckingMode": "basic",
|
||||||
|
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||||
|
"python-envs.pythonProjects": []
|
||||||
|
}
|
||||||
175
PLUGINS.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# 蛋定助手插件文档
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
蛋定助手是一个基于NoneBot2框架开发的QQ机器人,提供多种功能插件,包括AI聊天、管理API、自动撤回消息等。该机器人主要面向特定用户群体,提供游戏辅助和社群管理功能。
|
||||||
|
|
||||||
|
## 插件总览
|
||||||
|
|
||||||
|
| 插件名称 | 描述 | 权限要求 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| chatai | AI聊天功能,对接DeepSeek | 所有用户 |
|
||||||
|
| auto_recall | 消息自动撤回 | 系统自动执行 |
|
||||||
|
| damo_balance | 大漠账户余额查询 | 特定用户 |
|
||||||
|
| danding_api | 蛋定助手管理API | 超级用户 |
|
||||||
|
| danding_help | 帮助信息 | 特定群用户 |
|
||||||
|
| command_list | 命令列表管理 | 系统使用 |
|
||||||
|
|
||||||
|
## 详细插件文档
|
||||||
|
|
||||||
|
### 1. chatai - AI聊天插件
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
基于DeepSeek AI的聊天功能,支持将AI回复转换为图片形式,并在一定时间后自动撤回。
|
||||||
|
|
||||||
|
#### 使用方法
|
||||||
|
- 发送以 `*` 开头的消息触发AI回复
|
||||||
|
- AI回复会自动转为图片显示
|
||||||
|
- 回复会在120秒后自动撤回
|
||||||
|
|
||||||
|
#### 配置项
|
||||||
|
```env
|
||||||
|
DEEPSEEK_TOKEN=你的DeepSeek API密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 技术实现
|
||||||
|
- 使用OpenAI客户端连接DeepSeek API
|
||||||
|
- 使用Pyppeteer将Markdown转为图片
|
||||||
|
- 内置Chrome浏览器实例管理
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
用户: *你好,请介绍一下自己
|
||||||
|
AI: [图片形式回复] 👋 你好呀!我是蛋定助手,一个活泼可爱的AI助手!😊 很高兴认识你!有什么我能帮到你的吗?✨
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. auto_recall - 自动撤回插件
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
监控所有发出的消息,并在指定时间后自动撤回,保持聊天环境整洁。
|
||||||
|
|
||||||
|
#### 使用方法
|
||||||
|
- 无需手动调用,插件会自动监控并撤回消息
|
||||||
|
|
||||||
|
#### 配置项
|
||||||
|
```env
|
||||||
|
RECALL_DELAY=110 # 撤回延迟时间,单位为秒
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 技术实现
|
||||||
|
- 使用NoneBot的API拦截功能
|
||||||
|
- 异步定时任务管理
|
||||||
|
- 错误处理与日志记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. damo_balance - 大漠账户余额查询
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
查询大漠平台账户余额,需要验证码验证。
|
||||||
|
|
||||||
|
#### 使用方法
|
||||||
|
- 命令:`大漠余额`或`余额查询`
|
||||||
|
- 需要验证码验证
|
||||||
|
|
||||||
|
#### 权限要求
|
||||||
|
- 仅特定用户(ID:1424473282)可使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. danding_api - 蛋定助手管理API
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
提供管理员操作接口,包括在线人数查询、卡密管理和用户时长管理功能。
|
||||||
|
|
||||||
|
#### 使用方法
|
||||||
|
主要命令:
|
||||||
|
- `在线人数`:查询当前在线用户数
|
||||||
|
- `添加卡密 [类型] [卡密]`:添加指定类型的卡密
|
||||||
|
- `生成卡密 [类型]`:生成新卡密
|
||||||
|
- `用户加时 [用户名] [类型]`:为指定用户增加使用时长
|
||||||
|
|
||||||
|
#### 卡密类型
|
||||||
|
- 天卡/day/Day/DAY/天
|
||||||
|
- 周卡/week/Week/WEEK/周
|
||||||
|
- 月卡/month/Month/MONTH/月
|
||||||
|
|
||||||
|
#### 权限要求
|
||||||
|
- 仅超级用户可使用
|
||||||
|
|
||||||
|
#### 配置项
|
||||||
|
```env
|
||||||
|
SUPERUSERS=["1424473282"] # 超级用户ID列表
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
```
|
||||||
|
在线人数
|
||||||
|
> 当前在线用户数: 42
|
||||||
|
|
||||||
|
添加卡密 天卡 ABCD1234
|
||||||
|
> 添加卡密成功:天卡 ABCD1234
|
||||||
|
|
||||||
|
生成卡密 周卡
|
||||||
|
> 生成卡密成功:周卡 XYZ789ABC
|
||||||
|
|
||||||
|
用户加时 test_user 月卡
|
||||||
|
> 用户加时成功:test_user 增加了30天
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. danding_help - 帮助信息
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
提供各种帮助信息和指南,支持图片形式的教程和指引。
|
||||||
|
|
||||||
|
#### 使用方法
|
||||||
|
主要命令:
|
||||||
|
- `帮助`:显示帮助菜单
|
||||||
|
- `下载`:显示下载信息
|
||||||
|
- `公益版`/`正式版`:显示版本信息
|
||||||
|
- `正式版御魂双开`:显示双开教程
|
||||||
|
- `正式版如何运行`:显示运行教程
|
||||||
|
- `正式版内测计划`:显示内测信息
|
||||||
|
|
||||||
|
#### 权限要求
|
||||||
|
- 仅在特定群(621016172)可用
|
||||||
|
|
||||||
|
#### 技术实现
|
||||||
|
- 使用图片回复提供直观的教程
|
||||||
|
- 文本与图片混合响应
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. command_list - 命令列表管理
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
管理系统命令列表,提供命令过滤和权限控制。
|
||||||
|
|
||||||
|
#### 使用方法
|
||||||
|
- 系统内部使用,不直接暴露给用户
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q1: 如何启动蛋定助手?
|
||||||
|
A1: 使用`nb run`命令启动,确保已安装所有依赖。
|
||||||
|
|
||||||
|
### Q2: 机器人回复后自动撤回的时间可以修改吗?
|
||||||
|
A2: 可以,在`.env`文件中修改`RECALL_DELAY`的值(单位为秒)。
|
||||||
|
|
||||||
|
### Q3: 如何成为超级用户?
|
||||||
|
A3: 在`.env`文件的`SUPERUSERS`列表中添加您的QQ号。
|
||||||
|
|
||||||
|
### Q4: AI聊天功能如何配置?
|
||||||
|
A4: 需要在`.env`文件中设置`DEEPSEEK_TOKEN`,填入您的DeepSeek API密钥。
|
||||||
|
|
||||||
|
### Q5: 为什么帮助命令在某些群不可用?
|
||||||
|
A5: 帮助命令仅在特定群(621016172)内可用,这是一种权限控制机制。
|
||||||
|
|
||||||
|
## 技术支持
|
||||||
|
|
||||||
|
如有问题,可以:
|
||||||
|
1. 在群内@机器人并提问
|
||||||
|
2. 访问帮助文档:https://www.danding.icu
|
||||||
|
3. 联系超级用户获取支持
|
||||||
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# danding-bot
|
||||||
|
基于:[NoneBot](https://nonebot.dev/)
|
||||||
|
|
||||||
|
## 食用步骤
|
||||||
|
- 需要安装的Python版本:3.10.12,建议使用Anaconda虚拟环境
|
||||||
|
- `conda create --name bot python=3.10.12`
|
||||||
|
- `pip install -r requirements.txt`
|
||||||
|
- `nb run`
|
||||||
|
|
||||||
|
## 创建插件
|
||||||
|
使用 `nb plugin create` 快速创建空插件
|
||||||
|
|
||||||
|
## 已经安装的包
|
||||||
|
|
||||||
|
## 已安装插件列表
|
||||||
|
### 1. nonebot_plugin_withdraw - 消息撤回
|
||||||
|
- 回复机器人指定的语句,发送撤回即可
|
||||||
|
|
||||||
|
### 2. nonebot_plugin_learning_chat - 群聊学习
|
||||||
|
> [项目链接](https://github.com/CMHopeSunshine/nonebot-plugin-learning-chat)
|
||||||
|
- 功能启停:@Bot 开启学习\学说话\快学 \关闭学习\别学\闭嘴
|
||||||
|
- 禁用回复:@Bot 不可以\达咩\不能说这 [需回复机器人的发言]
|
||||||
|
- 后台管理:`http://127.0.0.1:8080/learning_chat/admin`
|
||||||
|
- 配置文件:`Bot目录/data/learing_chat/learning_chat.yml`
|
||||||
|
|
||||||
|
### 3. auto_recall - 消息自动撤回
|
||||||
|
- 自动监控所有发出的消息,在指定时间后撤回
|
||||||
|
- 默认配置:110秒后自动撤回
|
||||||
|
|
||||||
|
### 4. chatai - AI聊天
|
||||||
|
- 对接DeepSeek的聊天AI服务
|
||||||
|
- 使用方式:发送以`*`开头的消息触发AI回复
|
||||||
|
- 支持切换AI模型(目前仅支持deepseek)
|
||||||
|
- AI回复会在100秒后自动撤回
|
||||||
|
|
||||||
|
### 5. damo_balance - 大漠账户余额查询
|
||||||
|
- 命令:`大漠余额`或`余额查询`
|
||||||
|
- 需要验证码验证
|
||||||
|
- 仅特定用户(ID:1424473282)可使用
|
||||||
|
|
||||||
|
### 6. danding_api - 蛋定助手管理API
|
||||||
|
- 提供管理员操作接口
|
||||||
|
- 主要命令:
|
||||||
|
- `在线人数`:查询当前在线用户数
|
||||||
|
- `添加卡密`:添加指定类型的卡密
|
||||||
|
- `生成卡密`:生成新卡密
|
||||||
|
- `用户加时`:为指定用户增加使用时长
|
||||||
|
- 仅超级用户可用
|
||||||
|
|
||||||
|
### 7. danding_help - 帮助信息
|
||||||
|
- 主要命令:
|
||||||
|
- `帮助`:显示帮助菜单
|
||||||
|
- `下载`:显示下载信息
|
||||||
|
- `公益版`/`正式版`:显示版本信息
|
||||||
|
- `正式版御魂双开`:显示双开教程
|
||||||
|
- `正式版如何运行`:显示运行教程
|
||||||
|
- `正式版内测计划`:显示内测信息
|
||||||
|
- 仅在特定群(621016172)可用
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 该项目自带后台ui入口:`http://127.0.0.1:8080/learning_chat/admin`
|
||||||
|
- 配置路径在:`Bot目录/data/learing_chat/learning_chat.yml`
|
||||||
3
copyback.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
# 备份文件
|
||||||
|
rm NoneBot_DanDing_3.10.12.zip
|
||||||
|
zip -r NoneBot_DanDing_3.10.12.zip *
|
||||||
14
danding_bot/plugins/auto_friend_accept/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from nonebot import get_plugin_config
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from . import auto_accept
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="auto_friend_accept",
|
||||||
|
description="自动同意好友请求插件",
|
||||||
|
usage="""
|
||||||
|
# 自动好友请求接受插件
|
||||||
|
当收到好友请求时,会自动同意
|
||||||
|
|
||||||
|
无需用户操作,插件自动处理所有好友请求
|
||||||
|
""",
|
||||||
|
)
|
||||||
48
danding_bot/plugins/auto_friend_accept/auto_accept.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from nonebot import on_request, get_plugin_config, logger
|
||||||
|
from nonebot.adapters.onebot.v11 import FriendRequestEvent, Bot
|
||||||
|
from nonebot.typing import T_State
|
||||||
|
from .config import Config
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
|
||||||
|
# 获取插件配置
|
||||||
|
plugin_config = get_plugin_config(Config)
|
||||||
|
|
||||||
|
# 注册好友请求事件处理器
|
||||||
|
friend_request = on_request(priority=5, block=True)
|
||||||
|
|
||||||
|
@friend_request.handle()
|
||||||
|
async def handle_friend_request(bot: Bot, event: FriendRequestEvent, state: T_State):
|
||||||
|
"""处理好友请求,根据配置自动同意并发送欢迎消息"""
|
||||||
|
|
||||||
|
# 检查是否启用自动同意
|
||||||
|
if not plugin_config.auto_accept_enabled:
|
||||||
|
logger.info(f"收到来自 {event.user_id} 的好友请求,但自动同意功能已禁用")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取请求的标识信息
|
||||||
|
flag = event.flag
|
||||||
|
|
||||||
|
# 调用OneBot接口处理好友请求(设置为同意)
|
||||||
|
await bot.set_friend_add_request(flag=flag, approve=True)
|
||||||
|
|
||||||
|
logger.info(f"已自动同意来自 {event.user_id} 的好友请求")
|
||||||
|
|
||||||
|
# 如果配置了自动回复消息,则发送欢迎消息
|
||||||
|
if plugin_config.auto_reply_message:
|
||||||
|
# 添加随机延迟,模拟真人回复
|
||||||
|
await asyncio.sleep(random.uniform(2, 5))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 发送欢迎消息
|
||||||
|
await bot.send_private_msg(
|
||||||
|
user_id=event.user_id,
|
||||||
|
message=plugin_config.auto_reply_message
|
||||||
|
)
|
||||||
|
logger.info(f"已向新好友 {event.user_id} 发送欢迎消息")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"向新好友 {event.user_id} 发送欢迎消息失败: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理好友请求失败: {e}")
|
||||||
9
danding_bot/plugins/auto_friend_accept/config.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from pydantic import BaseModel, validator
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
# 是否启用自动同意好友请求
|
||||||
|
auto_accept_enabled: bool = True
|
||||||
|
|
||||||
|
# 自动回复的消息,如果为空则不发送
|
||||||
|
auto_reply_message: Optional[str] = ""
|
||||||
53
danding_bot/plugins/auto_recall/__init__.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from nonebot import get_driver, get_plugin_config, logger
|
||||||
|
from nonebot.adapters.onebot.v11 import Bot
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from nonebot.exception import MockApiException
|
||||||
|
from nonebot.adapters import Bot
|
||||||
|
from nonebot.typing import T_State
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
# 插件元信息
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="auto_recall",
|
||||||
|
description="一个通用的消息撤回插件,监控所有发出的消息并在指定时间后撤回",
|
||||||
|
usage="无需手动调用,插件会自动监控并撤回消息",
|
||||||
|
config=Config,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取插件配置
|
||||||
|
plugin_config = get_plugin_config(Config)
|
||||||
|
|
||||||
|
# 注册 API 调用后钩子
|
||||||
|
@Bot.on_called_api
|
||||||
|
async def handle_api_result(
|
||||||
|
bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any
|
||||||
|
):
|
||||||
|
"""拦截 send_msg API 调用,监控发出的消息"""
|
||||||
|
if api != "send_msg" or exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 获取消息 ID
|
||||||
|
message_id = result.get("message_id")
|
||||||
|
if not message_id:
|
||||||
|
logger.warning("未找到 message_id,无法撤回消息")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 获取撤回延迟时间
|
||||||
|
recall_delay = plugin_config.recall_delay
|
||||||
|
|
||||||
|
# 启动异步任务,延迟撤回消息
|
||||||
|
asyncio.create_task(recall_message_after_delay(bot, message_id, recall_delay))
|
||||||
|
|
||||||
|
async def recall_message_after_delay(bot: Bot, message_id: int, delay: int):
|
||||||
|
"""在指定时间后撤回消息"""
|
||||||
|
await asyncio.sleep(delay) # 等待指定时间
|
||||||
|
try:
|
||||||
|
await bot.delete_msg(message_id=message_id) # 撤回消息
|
||||||
|
except Exception as e:
|
||||||
|
if "success" in str(e).lower() or "timeout" in str(e).lower():
|
||||||
|
# 忽略成功和超时的错误
|
||||||
|
return
|
||||||
|
logger.error(f"撤回消息失败: {str(e)}")
|
||||||
4
danding_bot/plugins/auto_recall/config.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
recall_delay: int = Field(default=110, env="RECALL_DELAY") # 撤回延迟时间,默认 110 秒
|
||||||
183
danding_bot/plugins/chatai/__init__.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import atexit
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from nonebot import on_message, get_plugin_config, get_driver
|
||||||
|
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from nonebot.exception import FinishedException
|
||||||
|
from openai import OpenAI
|
||||||
|
from .config import Config
|
||||||
|
from .utils.text_image import create_text_image
|
||||||
|
from .screenshot import markdown_to_image
|
||||||
|
import pyppeteer
|
||||||
|
import pyppeteer.launcher
|
||||||
|
import types
|
||||||
|
|
||||||
|
# 插件元信息
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="chatai",
|
||||||
|
description="一个对接 DeepSeek 的聊天 AI 插件",
|
||||||
|
usage="发送以 * 开头的消息,AI 会回复你,两分钟后自动撤销",
|
||||||
|
config=Config,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取插件配置
|
||||||
|
plugin_config = get_plugin_config(Config)
|
||||||
|
|
||||||
|
# 全局浏览器实例
|
||||||
|
browser = None
|
||||||
|
browser_lock = threading.Lock()
|
||||||
|
|
||||||
|
# 注册消息事件处理器
|
||||||
|
message_handler = on_message(priority=50, block=True)
|
||||||
|
|
||||||
|
# 确保输出目录存在
|
||||||
|
os.makedirs("data/chatai", exist_ok=True)
|
||||||
|
|
||||||
|
# 获取 NoneBot 驱动器
|
||||||
|
driver = get_driver()
|
||||||
|
|
||||||
|
# 定义强制终止 Chrome 的函数
|
||||||
|
def force_kill_chrome():
|
||||||
|
"""强制终止所有 Chrome 进程"""
|
||||||
|
try:
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
subprocess.run(['taskkill', '/F', '/IM', 'chrome.exe'],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
else:
|
||||||
|
subprocess.run(['pkill', '-9', '-f', 'chrome'],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 在启动时确保没有残留的 Chrome 进程
|
||||||
|
force_kill_chrome()
|
||||||
|
|
||||||
|
# 注册退出处理函数
|
||||||
|
atexit.register(force_kill_chrome)
|
||||||
|
|
||||||
|
# 注册信号处理
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
"""处理终止信号"""
|
||||||
|
# 直接强制终止 Chrome 进程,不使用 Pyppeteer 的关闭方法
|
||||||
|
force_kill_chrome()
|
||||||
|
# 强制退出程序
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
# 注册信号处理器
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
@driver.on_shutdown
|
||||||
|
async def close_browser():
|
||||||
|
"""在 NoneBot 关闭时关闭浏览器"""
|
||||||
|
global browser
|
||||||
|
with browser_lock:
|
||||||
|
if browser is not None:
|
||||||
|
try:
|
||||||
|
await browser.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
browser = None
|
||||||
|
# 确保所有 Chrome 进程都被终止
|
||||||
|
force_kill_chrome()
|
||||||
|
|
||||||
|
# 替代方案:直接替换信号处理器
|
||||||
|
def noop_signal_handler(sig, frame):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 保存原始信号处理器
|
||||||
|
original_sigint = signal.getsignal(signal.SIGINT)
|
||||||
|
original_sigterm = signal.getsignal(signal.SIGTERM)
|
||||||
|
|
||||||
|
# 在启动浏览器前替换信号处理器
|
||||||
|
async def init_browser():
|
||||||
|
"""初始化浏览器实例"""
|
||||||
|
global browser
|
||||||
|
with browser_lock:
|
||||||
|
if browser is None or not hasattr(browser, 'process') or not browser.process:
|
||||||
|
# 替换信号处理器
|
||||||
|
signal.signal(signal.SIGINT, noop_signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, noop_signal_handler)
|
||||||
|
|
||||||
|
try:
|
||||||
|
browser = await pyppeteer.launch(
|
||||||
|
headless=True,
|
||||||
|
args=['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# 恢复我们的信号处理器
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
return browser
|
||||||
|
|
||||||
|
async def call_ai_api(message: str) -> str:
|
||||||
|
"""调用 AI 接口"""
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=plugin_config.deepseek_token,
|
||||||
|
base_url="https://api.siliconflow.cn/v1"
|
||||||
|
)
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="deepseek-ai/DeepSeek-V3",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "你是一个活泼可爱的群助手。请在回复中使用丰富的 Emoji 表情(如 😊 😄 🎉 ✨ 💖 等)。你的语气要俏皮活泼,像在和朋友聊天一样自然。在回答问题时要保持专业性的同时,也要让回复显得生动有趣。每条回复都必须包含至少2-3个 Emoji 表情。如果你需要展示代码片段,请确保代码中不包含任何颜文字或表情符号,保持代码的专业性和可读性。"},
|
||||||
|
{"role": "user", "content": message},
|
||||||
|
],
|
||||||
|
stream=False
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content or ""
|
||||||
|
|
||||||
|
@message_handler.handle()
|
||||||
|
async def handle_message(event: MessageEvent, bot: Bot):
|
||||||
|
# 获取用户发送的消息内容
|
||||||
|
user_message = event.get_plaintext().strip()
|
||||||
|
|
||||||
|
# 检查消息是否以 * 开头
|
||||||
|
if not user_message.startswith("*"):
|
||||||
|
return # 如果不是以 * 开头,直接返回,不处理
|
||||||
|
|
||||||
|
# 去掉开头的 * 并去除多余空格
|
||||||
|
user_message = user_message[1:].strip()
|
||||||
|
|
||||||
|
# 如果消息为空,直接返回
|
||||||
|
if not user_message:
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await message_handler.finish("请输入有效内容哦~")
|
||||||
|
|
||||||
|
# 调用模型 API
|
||||||
|
try:
|
||||||
|
# 初始化浏览器
|
||||||
|
browser = await init_browser()
|
||||||
|
|
||||||
|
# 调用 AI API
|
||||||
|
response = await call_ai_api(user_message)
|
||||||
|
|
||||||
|
if response:
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
# 使用 markdown_to_image 生成图片
|
||||||
|
image_path = 'data/chatai/output.png'
|
||||||
|
await markdown_to_image(response, image_path, browser)
|
||||||
|
|
||||||
|
# 发送图片消息
|
||||||
|
sent_message = await bot.send(event, MessageSegment.image(f"file:///{os.path.abspath(image_path)}"))
|
||||||
|
|
||||||
|
# 启动定时任务,两分钟后撤销消息
|
||||||
|
asyncio.create_task(delete_message_after_delay(bot, sent_message["message_id"]))
|
||||||
|
except FinishedException:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await message_handler.finish(f"出错了: {str(e)}")
|
||||||
|
|
||||||
|
async def delete_message_after_delay(bot: Bot, message_id: int):
|
||||||
|
"""两分钟后撤销消息"""
|
||||||
|
await asyncio.sleep(120) # 等待两分钟
|
||||||
|
try:
|
||||||
|
await bot.delete_msg(message_id=message_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
26
danding_bot/plugins/chatai/chrome_manager.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def kill_chrome_processes():
|
||||||
|
"""强制终止所有 Chrome 进程"""
|
||||||
|
try:
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
subprocess.run(['taskkill', '/F', '/IM', 'chrome.exe'],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
subprocess.run(['taskkill', '/F', '/IM', 'chromedriver.exe'],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
else:
|
||||||
|
# 使用 pkill 终止所有 Chrome 相关进程
|
||||||
|
subprocess.run(['pkill', '-9', '-f', 'chrome'],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
subprocess.run(['pkill', '-9', '-f', 'chromium'],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "kill":
|
||||||
|
kill_chrome_processes()
|
||||||
6
danding_bot/plugins/chatai/config.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
"""Plugin Config Here"""
|
||||||
|
deepseek_token: str = Field(..., env="DEEPSEEK_TOKEN")
|
||||||
|
# grok_token: str = Field(..., env="GROK_TOKEN")
|
||||||
164
danding_bot/plugins/chatai/screenshot.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import asyncio
|
||||||
|
import markdown
|
||||||
|
from pyppeteer import launch
|
||||||
|
|
||||||
|
async def markdown_to_image(markdown_text: str, output_path: str, browser=None):
|
||||||
|
"""将 Markdown 转换为 HTML 并使用 Puppeteer 截图。"""
|
||||||
|
try:
|
||||||
|
# 将 Markdown 转换为 HTML
|
||||||
|
html = markdown.markdown(markdown_text)
|
||||||
|
|
||||||
|
# 使用传入的浏览器实例或创建新的
|
||||||
|
should_close_browser = False
|
||||||
|
if browser is None:
|
||||||
|
browser = await launch(headless=True, args=['--no-sandbox', '--disable-setuid-sandbox'])
|
||||||
|
should_close_browser = True
|
||||||
|
|
||||||
|
page = await browser.newPage()
|
||||||
|
|
||||||
|
# 设置页面样式,使内容更美观
|
||||||
|
await page.setContent(f"""
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 30px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #333;
|
||||||
|
}}
|
||||||
|
.container {{
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 25px;
|
||||||
|
overflow: hidden;
|
||||||
|
}}
|
||||||
|
p {{
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}}
|
||||||
|
code {{
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
background-color: #f5f7f9;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}}
|
||||||
|
pre {{
|
||||||
|
background-color: #f5f7f9;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 20px 0;
|
||||||
|
}}
|
||||||
|
pre code {{
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}}
|
||||||
|
h1, h2, h3, h4, h5, h6 {{
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
}}
|
||||||
|
h1 {{
|
||||||
|
font-size: 1.8em;
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}}
|
||||||
|
h2 {{
|
||||||
|
font-size: 1.5em;
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}}
|
||||||
|
blockquote {{
|
||||||
|
padding: 0 1em;
|
||||||
|
color: #6a737d;
|
||||||
|
border-left: 0.25em solid #dfe2e5;
|
||||||
|
margin: 16px 0;
|
||||||
|
}}
|
||||||
|
ul, ol {{
|
||||||
|
padding-left: 2em;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}}
|
||||||
|
img {{
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}}
|
||||||
|
a {{
|
||||||
|
color: #0366d6;
|
||||||
|
text-decoration: none;
|
||||||
|
}}
|
||||||
|
a:hover {{
|
||||||
|
text-decoration: underline;
|
||||||
|
}}
|
||||||
|
table {{
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 16px 0;
|
||||||
|
}}
|
||||||
|
table th, table td {{
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #dfe2e5;
|
||||||
|
}}
|
||||||
|
table th {{
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
{html}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 等待内容渲染完成
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# 获取内容尺寸并设置视口
|
||||||
|
dimensions = await page.evaluate('''() => {
|
||||||
|
const container = document.querySelector('.container');
|
||||||
|
return {
|
||||||
|
width: container.offsetWidth + 60, // 加上 body 的 padding
|
||||||
|
height: container.offsetHeight + 60
|
||||||
|
}
|
||||||
|
}''')
|
||||||
|
|
||||||
|
# 设置视口大小
|
||||||
|
await page.setViewport({
|
||||||
|
'width': dimensions['width'],
|
||||||
|
'height': dimensions['height'],
|
||||||
|
'deviceScaleFactor': 2 # 提高图片清晰度
|
||||||
|
})
|
||||||
|
|
||||||
|
# 截图,使用透明背景
|
||||||
|
await page.screenshot({
|
||||||
|
'path': output_path,
|
||||||
|
'omitBackground': True, # 透明背景
|
||||||
|
'clip': {
|
||||||
|
'x': 0,
|
||||||
|
'y': 0,
|
||||||
|
'width': dimensions['width'],
|
||||||
|
'height': dimensions['height']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 关闭页面
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
# 如果是我们创建的浏览器实例,则关闭它
|
||||||
|
if should_close_browser:
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 确保资源被释放
|
||||||
|
if 'page' in locals() and page is not None:
|
||||||
|
await page.close()
|
||||||
|
if should_close_browser and 'browser' in locals() and browser is not None:
|
||||||
|
await browser.close()
|
||||||
|
raise # 重新抛出异常以便上层处理
|
||||||
1
danding_bot/plugins/chatai/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""文本处理工具包"""
|
||||||
143
danding_bot/plugins/chatai/utils/text_image.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
import io
|
||||||
|
|
||||||
|
def create_text_image(text: str, width: int = 1000, font_size: int = 25) -> bytes:
|
||||||
|
"""将文本转换为图片,智能处理各种字符"""
|
||||||
|
def load_fonts():
|
||||||
|
"""加载文本和 Emoji 字体"""
|
||||||
|
# 尝试加载 Emoji 字体
|
||||||
|
emoji_font = None
|
||||||
|
try:
|
||||||
|
emoji_font = ImageFont.truetype("/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf", font_size)
|
||||||
|
print("成功加载 Emoji 字体")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"加载 Emoji 字体失败: {e}")
|
||||||
|
|
||||||
|
# 尝试加载文本字体
|
||||||
|
text_font = None
|
||||||
|
font_paths = [
|
||||||
|
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||||
|
"/usr/share/fonts/wqy-microhei/wqy-microhei.ttc",
|
||||||
|
"/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc",
|
||||||
|
"C:/Windows/Fonts/msyh.ttc",
|
||||||
|
"C:/Windows/Fonts/simhei.ttf",
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in font_paths:
|
||||||
|
try:
|
||||||
|
text_font = ImageFont.truetype(path, font_size)
|
||||||
|
print(f"成功加载文本字体: {path}")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if text_font is None:
|
||||||
|
text_font = ImageFont.load_default()
|
||||||
|
print("使用默认字体")
|
||||||
|
|
||||||
|
return text_font, emoji_font
|
||||||
|
|
||||||
|
def is_emoji(char):
|
||||||
|
"""判断字符是否为 Emoji"""
|
||||||
|
return len(char.encode('utf-8')) >= 4
|
||||||
|
|
||||||
|
def draw_text_with_fonts(draw, text, x, y, text_font, emoji_font):
|
||||||
|
"""使用不同的字体绘制文本和 Emoji"""
|
||||||
|
current_x = x
|
||||||
|
for char in text:
|
||||||
|
# 选择合适的字体
|
||||||
|
font = emoji_font if (is_emoji(char) and emoji_font) else text_font
|
||||||
|
|
||||||
|
# 绘制字符
|
||||||
|
draw.text((current_x, y), char, font=font, fill=(0, 0, 0))
|
||||||
|
|
||||||
|
# 计算字符宽度
|
||||||
|
bbox = draw.textbbox((current_x, y), char, font=font)
|
||||||
|
char_width = bbox[2] - bbox[0]
|
||||||
|
current_x += char_width
|
||||||
|
|
||||||
|
return current_x - x
|
||||||
|
|
||||||
|
def calculate_text_dimensions(text, text_font, emoji_font):
|
||||||
|
"""计算文本尺寸"""
|
||||||
|
test_img = Image.new('RGB', (1, 1), color=(255, 255, 255))
|
||||||
|
test_draw = ImageDraw.Draw(test_img)
|
||||||
|
|
||||||
|
total_width = 0
|
||||||
|
max_height = 0
|
||||||
|
|
||||||
|
for char in text:
|
||||||
|
font = emoji_font if (is_emoji(char) and emoji_font) else text_font
|
||||||
|
bbox = test_draw.textbbox((0, 0), char, font=font)
|
||||||
|
char_width = bbox[2] - bbox[0]
|
||||||
|
char_height = bbox[3] - bbox[1]
|
||||||
|
total_width += char_width
|
||||||
|
max_height = max(max_height, char_height)
|
||||||
|
|
||||||
|
return total_width, max_height
|
||||||
|
|
||||||
|
# 加载字体
|
||||||
|
text_font, emoji_font = load_fonts()
|
||||||
|
|
||||||
|
# 基础配置
|
||||||
|
padding = 40
|
||||||
|
effective_width = width - (2 * padding)
|
||||||
|
|
||||||
|
def smart_text_wrap(text):
|
||||||
|
"""智能文本换行"""
|
||||||
|
lines = []
|
||||||
|
current_line = ""
|
||||||
|
current_width = 0
|
||||||
|
|
||||||
|
for paragraph in text.split('\n'):
|
||||||
|
if not paragraph:
|
||||||
|
lines.append("")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for char in paragraph:
|
||||||
|
char_width, _ = calculate_text_dimensions(char, text_font, emoji_font)
|
||||||
|
|
||||||
|
if current_width + char_width <= effective_width:
|
||||||
|
current_line += char
|
||||||
|
current_width += char_width
|
||||||
|
else:
|
||||||
|
if current_line:
|
||||||
|
lines.append(current_line)
|
||||||
|
current_line = char
|
||||||
|
current_width = char_width
|
||||||
|
|
||||||
|
if current_line:
|
||||||
|
lines.append(current_line)
|
||||||
|
current_line = ""
|
||||||
|
current_width = 0
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
# 智能换行处理
|
||||||
|
lines = smart_text_wrap(text)
|
||||||
|
|
||||||
|
# 计算行高
|
||||||
|
_, line_height = calculate_text_dimensions("测试", text_font, emoji_font)
|
||||||
|
line_spacing = int(line_height * 0.5) # 行间距为行高的50%
|
||||||
|
total_line_height = line_height + line_spacing
|
||||||
|
|
||||||
|
# 计算总高度
|
||||||
|
total_height = (len(lines) * total_line_height) + (2 * padding)
|
||||||
|
height = max(total_height, 200) # 最小高度200像素
|
||||||
|
|
||||||
|
# 创建图片
|
||||||
|
image = Image.new('RGB', (width, int(height)), color=(252, 252, 252))
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
|
# 绘制文本
|
||||||
|
y = padding
|
||||||
|
for line in lines:
|
||||||
|
if line: # 跳过空行
|
||||||
|
draw_text_with_fonts(draw, line, padding, y, text_font, emoji_font)
|
||||||
|
y += total_line_height
|
||||||
|
|
||||||
|
# 转换为字节流
|
||||||
|
img_byte_arr = io.BytesIO()
|
||||||
|
image.save(img_byte_arr, format='PNG', quality=95)
|
||||||
|
img_byte_arr.seek(0)
|
||||||
|
return img_byte_arr.getvalue()
|
||||||
4
danding_bot/plugins/command_list/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from . import command_list
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
__plugin_meta__ = Config
|
||||||
50
danding_bot/plugins/command_list/command_list.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from nonebot import on_command, get_loaded_plugins, logger
|
||||||
|
from nonebot.rule import fullmatch
|
||||||
|
from nonebot.adapters.onebot.v11.event import MessageEvent
|
||||||
|
from nonebot.plugin import Plugin
|
||||||
|
from nonebot_plugin_saa import Text, MessageFactory
|
||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
ALLOWED_USER = 1424473282
|
||||||
|
|
||||||
|
async def check_user(event: MessageEvent) -> bool:
|
||||||
|
"""检查用户是否有权限使用该命令"""
|
||||||
|
return event.user_id == ALLOWED_USER
|
||||||
|
|
||||||
|
cmd = on_command(
|
||||||
|
"指令列表",
|
||||||
|
rule=check_user and fullmatch(("指令列表", "命令列表", "help list", "cmd list")),
|
||||||
|
aliases={"命令列表", "help list", "cmd list"},
|
||||||
|
priority=1,
|
||||||
|
block=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def format_plugin_info(plugin: Plugin) -> str:
|
||||||
|
"""格式化插件信息"""
|
||||||
|
info = []
|
||||||
|
if hasattr(plugin, "metadata") and plugin.metadata:
|
||||||
|
meta = plugin.metadata
|
||||||
|
if hasattr(meta, "name") and meta.name:
|
||||||
|
info.append(f"插件名称: {meta.name}")
|
||||||
|
if hasattr(meta, "description") and meta.description:
|
||||||
|
info.append(f"功能描述: {meta.description}")
|
||||||
|
if hasattr(meta, "usage") and meta.usage:
|
||||||
|
info.append(f"使用方法: {meta.usage}")
|
||||||
|
return "\n".join(info) if info else f"插件: {plugin.name}"
|
||||||
|
|
||||||
|
@cmd.handle()
|
||||||
|
async def handle_command_list():
|
||||||
|
plugins = get_loaded_plugins()
|
||||||
|
msg_parts = ["当前支持的指令列表:\n"]
|
||||||
|
|
||||||
|
for plugin in plugins:
|
||||||
|
plugin_info = format_plugin_info(plugin)
|
||||||
|
if plugin_info:
|
||||||
|
msg_parts.append(f"\n{plugin_info}\n{'='*30}")
|
||||||
|
|
||||||
|
await asyncio.sleep(random.uniform(1, 2))
|
||||||
|
await MessageFactory([Text("\n".join(msg_parts))]).send(
|
||||||
|
at_sender=True,
|
||||||
|
reply=True
|
||||||
|
)
|
||||||
8
danding_bot/plugins/command_list/config.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
"""命令列表插件配置"""
|
||||||
|
plugin_name: str = "命令列表"
|
||||||
|
plugin_description: str = "获取当前机器人支持的所有指令"
|
||||||
|
plugin_usage: str = "发送 '指令列表' 获取所有支持的指令"
|
||||||
|
plugin_author: str = "Assistant"
|
||||||
81
danding_bot/plugins/damo_balance/AccountSpider.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
|
||||||
|
class AccountSpider:
|
||||||
|
def __init__(self):
|
||||||
|
self.base_url = "http://121.204.253.175:8088"
|
||||||
|
self.session = requests.Session()
|
||||||
|
# 设置默认请求头
|
||||||
|
self.session.headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_verification_code(self,onlysave = False):
|
||||||
|
"""获取并保存验证码图片"""
|
||||||
|
code_url = f"{self.base_url}/code.asp"
|
||||||
|
response = self.session.get(code_url)
|
||||||
|
|
||||||
|
# 保存验证码图片
|
||||||
|
image = Image.open(io.BytesIO(response.content))
|
||||||
|
image.save('/bot/danding-bot/danding_bot/plugins/damo_balance/verification_code.png')
|
||||||
|
print("验证码图片已保存为 verification_code.png")
|
||||||
|
# 仅保存验证码图片
|
||||||
|
if onlysave:
|
||||||
|
return
|
||||||
|
# 等待用户输入验证码
|
||||||
|
return input("请输入验证码: ")
|
||||||
|
|
||||||
|
def login(self, username, password,v_code=""):
|
||||||
|
"""执行登录操作"""
|
||||||
|
|
||||||
|
# 获取验证码
|
||||||
|
if v_code:
|
||||||
|
verification_code = v_code
|
||||||
|
else:
|
||||||
|
verification_code = self.get_verification_code()
|
||||||
|
|
||||||
|
# 准备登录数据
|
||||||
|
login_data = {
|
||||||
|
'login_type': '0',
|
||||||
|
'f_user': username,
|
||||||
|
'f_code': password,
|
||||||
|
'codeOK': verification_code,
|
||||||
|
'Submit': '%C8%B7%B6%A8'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送登录请求
|
||||||
|
login_url = f"{self.base_url}/login_result.asp"
|
||||||
|
response = self.session.post(login_url, data=login_data)
|
||||||
|
response.encoding = 'gb2312' # 设置正确的编码
|
||||||
|
|
||||||
|
# 检查登录是否成功 - 通过检查是否包含重定向到account.asp的脚本
|
||||||
|
if "window.location.href=\"account.asp\"" in response.text:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_balance(self):
|
||||||
|
"""获取账户余额"""
|
||||||
|
account_url = f"{self.base_url}/account.asp"
|
||||||
|
response = self.session.get(account_url)
|
||||||
|
response.encoding = 'gb2312' # 设置正确的编码
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
balance_text = soup.find_all('span', class_='red')[1].text
|
||||||
|
return float(balance_text)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 账号密码配置
|
||||||
|
USERNAME = "xsllovemlj"
|
||||||
|
PASSWORD = "xsl1314520mlj"
|
||||||
|
|
||||||
|
spider = AccountSpider()
|
||||||
|
|
||||||
|
# 尝试登录
|
||||||
|
if spider.login(USERNAME, PASSWORD):
|
||||||
|
print("登录成功!")
|
||||||
|
balance = spider.get_balance()
|
||||||
|
print(f"账户余额:{balance}元")
|
||||||
|
else:
|
||||||
|
print("登录失败,请检查账号密码或验证码是否正确")
|
||||||
81
danding_bot/plugins/damo_balance/__init__.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from nonebot import get_plugin_config, on_command
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from nonebot.rule import to_me
|
||||||
|
from nonebot.adapters.onebot.v11 import Message,MessageEvent
|
||||||
|
from nonebot.params import ArgPlainText,CommandArg
|
||||||
|
from .config import Config
|
||||||
|
from nonebot.typing import T_State
|
||||||
|
from .AccountSpider import AccountSpider
|
||||||
|
from nonebot_plugin_saa import Text, Image, MessageFactory
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="大漠余额查询",
|
||||||
|
description="查询大漠插件平台账户余额的插件",
|
||||||
|
usage="""
|
||||||
|
指令:
|
||||||
|
- 大漠余额
|
||||||
|
- 余额查询
|
||||||
|
|
||||||
|
权限:
|
||||||
|
仅限指定用户(QQ:1424473282)使用
|
||||||
|
|
||||||
|
使用流程:
|
||||||
|
1. 发送"大漠余额"或"余额查询"指令
|
||||||
|
2. 机器人会返回验证码图片
|
||||||
|
3. 输入验证码完成查询
|
||||||
|
""",
|
||||||
|
config=Config,
|
||||||
|
)
|
||||||
|
|
||||||
|
config = get_plugin_config(Config)
|
||||||
|
|
||||||
|
spider = AccountSpider()
|
||||||
|
|
||||||
|
# 指令:大漠余额
|
||||||
|
check_balance = on_command("大漠余额", aliases={"余额查询"}, priority=5,block=True)
|
||||||
|
|
||||||
|
@check_balance.handle()
|
||||||
|
async def handle_first_receive(event: MessageEvent, state: T_State):
|
||||||
|
user_id = event.user_id
|
||||||
|
if user_id not in [1424473282]:
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await check_balance.finish("你没有权限进行此操作")
|
||||||
|
|
||||||
|
global spider
|
||||||
|
spider = AccountSpider()
|
||||||
|
# 获取验证码并存储
|
||||||
|
spider.get_verification_code(True)
|
||||||
|
# 获取当前脚本所在目录的绝对路径
|
||||||
|
current_dir = os.path.dirname(__file__)
|
||||||
|
# 构造图片的绝对路径
|
||||||
|
image_path = os.path.join(current_dir, "verification_code.png")
|
||||||
|
# 发送图片
|
||||||
|
with open(image_path, "rb") as f:
|
||||||
|
image_bytes = f.read()
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await MessageFactory([Text("请发送验证码图片中的内容进行验证:"),Image(image_bytes)]).send()
|
||||||
|
|
||||||
|
# 验证用户输入的验证码
|
||||||
|
@check_balance.got("captcha", prompt="请输入验证码:")
|
||||||
|
async def handle_captcha(event: MessageEvent, state: T_State, captcha: str = ArgPlainText("captcha")):
|
||||||
|
user_id = event.user_id
|
||||||
|
if user_id not in [1424473282]:
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await check_balance.finish("你没有权限进行此操作")
|
||||||
|
|
||||||
|
# 账号密码配置
|
||||||
|
USERNAME = "xsllovemlj"
|
||||||
|
PASSWORD = "xsl1314520mlj"
|
||||||
|
|
||||||
|
global spider
|
||||||
|
if spider.login(USERNAME, PASSWORD, captcha):
|
||||||
|
print("登录成功!")
|
||||||
|
balance = spider.get_balance()
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await check_balance.finish(f"大漠账户余额:{balance}元")
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await check_balance.reject("获取失败、登录失败,请检查账号密码或验证码是否正确")
|
||||||
5
danding_bot/plugins/damo_balance/config.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
"""Plugin Config Here"""
|
||||||
BIN
danding_bot/plugins/damo_balance/verification_code.png
Normal file
|
After Width: | Height: | Size: 193 B |
22
danding_bot/plugins/danding_api/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from nonebot import get_driver
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from .config import Config
|
||||||
|
from . import admin
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="danding_api",
|
||||||
|
description="咸鸭蛋API管理插件,提供卡密管理和用户管理功能",
|
||||||
|
usage="""
|
||||||
|
超级用户指令:
|
||||||
|
/咸鸭蛋 (或 /apihelp, /sudhelp) - 显示帮助信息
|
||||||
|
/添加卡密 <类型> <卡密> (或 /addkami, /akm) - 添加指定类型的卡密
|
||||||
|
/生成卡密 <类型> (或 /createkami, /ckm) - 生成指定类型的卡密
|
||||||
|
/用户加时 <用户名> <类型> (或 /addviptime, /avt) - 为指定用户添加会员时间
|
||||||
|
/生成QQ验证码 <QQ号> (或 /qqvcode, /gqvc) - 生成QQ绑定验证码
|
||||||
|
|
||||||
|
普通用户指令:
|
||||||
|
/在线人数 (或 /ddonline, /ddop) - 查看当前在线人数
|
||||||
|
""",
|
||||||
|
config=Config,
|
||||||
|
)
|
||||||
|
|
||||||
142
danding_bot/plugins/danding_api/admin.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
from nonebot import on_command, get_plugin_config,logger
|
||||||
|
from nonebot.permission import SUPERUSER
|
||||||
|
from nonebot.rule import to_me
|
||||||
|
from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent, MessageSegment
|
||||||
|
from nonebot.params import Depends
|
||||||
|
from .config import Config
|
||||||
|
from .utils import post, get_classes, post_vcode, get_log
|
||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
plugin_config = get_plugin_config(Config)
|
||||||
|
|
||||||
|
|
||||||
|
help = on_command("咸鸭蛋",rule=to_me(),aliases={"apihelp", "sudhelp"},permission=SUPERUSER, priority=0, block=True)
|
||||||
|
@help.handle()
|
||||||
|
async def _():
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await help.finish(plugin_config.HelpStr)
|
||||||
|
|
||||||
|
ddonline = on_command("在线人数",rule=to_me(),aliases={"ddonline", "ddop"}, priority=0, block=True)
|
||||||
|
@ddonline.handle()
|
||||||
|
async def _(event:PrivateMessageEvent):
|
||||||
|
id:str = str(event.user_id)
|
||||||
|
msg:str = await post("在线人数",id)
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await ddonline.finish(msg)
|
||||||
|
|
||||||
|
addkami = on_command("添加卡密",rule=to_me(),aliases={"addkami", "akm"},permission=SUPERUSER, priority=0, block=True)
|
||||||
|
@addkami.handle()
|
||||||
|
async def _(event:PrivateMessageEvent):
|
||||||
|
id:str = str(event.user_id)
|
||||||
|
msg:str = event.get_plaintext()
|
||||||
|
if len(msg.split(' ')) != 3:
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await ddonline.finish("参数不正确!")
|
||||||
|
|
||||||
|
classes:str = msg.split(' ')[1]
|
||||||
|
classes = get_classes(classes)
|
||||||
|
if classes == '':
|
||||||
|
await ddonline.finish("卡密类型不正确!")
|
||||||
|
|
||||||
|
kami:str = msg.split(' ')[2]
|
||||||
|
msg:str = await post("添加卡密",id,{
|
||||||
|
"classes":classes,
|
||||||
|
"kami":kami
|
||||||
|
})
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await ddonline.finish(msg)
|
||||||
|
|
||||||
|
createkami = on_command("生成卡密",rule=to_me(),aliases={"createkami", "ckm"},permission=SUPERUSER, priority=0, block=True)
|
||||||
|
@createkami.handle()
|
||||||
|
async def _(event:PrivateMessageEvent):
|
||||||
|
id:str = str(event.user_id)
|
||||||
|
msg:str = event.get_plaintext()
|
||||||
|
if len(msg.split(' ')) != 2:
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await ddonline.finish("参数不正确!")
|
||||||
|
|
||||||
|
classes:str = msg.split(' ')[1]
|
||||||
|
classes = get_classes(classes)
|
||||||
|
if classes == '':
|
||||||
|
await ddonline.finish("卡密类型不正确!")
|
||||||
|
|
||||||
|
msg:str = await post("生成卡密",id,{
|
||||||
|
"classes":classes
|
||||||
|
})
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await ddonline.finish(msg)
|
||||||
|
|
||||||
|
addviptime = on_command("用户加时",rule=to_me(),aliases={"addviptime", "avt"},permission=SUPERUSER, priority=0, block=True)
|
||||||
|
@addviptime.handle()
|
||||||
|
async def _(event:PrivateMessageEvent):
|
||||||
|
id:str = str(event.user_id)
|
||||||
|
msg:str = event.get_plaintext()
|
||||||
|
if len(msg.split(' ')) != 3:
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await ddonline.finish("参数不正确!")
|
||||||
|
|
||||||
|
username:str = msg.split(' ')[1]
|
||||||
|
classes:str = msg.split(' ')[2]
|
||||||
|
classes = get_classes(classes)
|
||||||
|
if classes == '':
|
||||||
|
await ddonline.finish("卡密类型不正确!")
|
||||||
|
|
||||||
|
msg:str = await post("用户加时",id,{
|
||||||
|
"username":username,
|
||||||
|
"classes":classes
|
||||||
|
})
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await ddonline.finish(msg)
|
||||||
|
|
||||||
|
generate_qq_vcode = on_command("绑定QQ",aliases={"bindqq", "绑定qq"}, priority=0, block=True)
|
||||||
|
|
||||||
|
# 添加用户使用时间记录字典
|
||||||
|
user_last_use_time = {}
|
||||||
|
|
||||||
|
@generate_qq_vcode.handle()
|
||||||
|
async def _(event: GroupMessageEvent): # GroupMessageEvent PrivateMessageEvent
|
||||||
|
# 检查是否来自指定群组
|
||||||
|
if event.group_id != 621016172:
|
||||||
|
return
|
||||||
|
# if event.user_id != 1424473282:
|
||||||
|
# return
|
||||||
|
|
||||||
|
id:str = str(event.user_id)
|
||||||
|
|
||||||
|
# 限流检查:检查用户上次使用时间
|
||||||
|
current_time = time.time()
|
||||||
|
if id in user_last_use_time:
|
||||||
|
time_diff = current_time - user_last_use_time[id]
|
||||||
|
if time_diff < 60: # 60秒内已使用过
|
||||||
|
await generate_qq_vcode.finish(f"请求过于频繁,请在{int(60 - time_diff)}秒后再试")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 更新用户最后使用时间
|
||||||
|
user_last_use_time[id] = current_time
|
||||||
|
|
||||||
|
msg:str = await post_vcode(id)
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
# 在消息前添加@用户
|
||||||
|
at_user = MessageSegment.at(event.user_id)
|
||||||
|
await generate_qq_vcode.finish(at_user + " " + msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
view_logs = on_command("查看日志",aliases={"logs", "查询日志"}, priority=0, block=True)
|
||||||
|
@view_logs.handle()
|
||||||
|
async def _(event:GroupMessageEvent): # GroupMessageEvent PrivateMessageEvent
|
||||||
|
# 检查是否来自指定群组
|
||||||
|
if event.group_id != 621016172:
|
||||||
|
return
|
||||||
|
# if event.user_id != 1424473282:
|
||||||
|
# return
|
||||||
|
|
||||||
|
id:str = str(event.user_id)
|
||||||
|
msg:str = await get_log(id)
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
# 在消息前添加@用户
|
||||||
|
at_user = MessageSegment.at(event.user_id)
|
||||||
|
await view_logs.finish(at_user + " " + msg)
|
||||||
28
danding_bot/plugins/danding_api/config.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
"""Plugin Config Here"""
|
||||||
|
|
||||||
|
HelpStr:str = """
|
||||||
|
这是一个蛋定助手的RoBot控制插件,功能菜单:
|
||||||
|
在线人数 : 查询当前蛋定助手在线用户数量;
|
||||||
|
添加卡密 [天|周|月] [指定卡密]: 添加一张指定天数的指定卡密;
|
||||||
|
生成卡密 [天|周|月]: 生成一张指定天数的卡密;
|
||||||
|
用户加时 [用户名] [天|周|月] : 添加指定用户时长;
|
||||||
|
绑定QQ: 为当前QQ号生成绑定验证码;
|
||||||
|
查看日志: 查看当前QQ号绑定日志;
|
||||||
|
"""
|
||||||
|
|
||||||
|
Token:str = "3340e353a49447f1be640543cbdcd937"
|
||||||
|
"""对接服务器的Token"""
|
||||||
|
|
||||||
|
DDApi_Host:str = "https://api.danding.vip/DD/" # https://api.danding.vip/DD/ http://192.168.5.11:8002/DD/
|
||||||
|
"""蛋定服务器连接地址 必须指向DD路由(开发环境)"""
|
||||||
|
|
||||||
|
# 邮件设置
|
||||||
|
EMAIL_API: str = "https://pmail.danding.vip/api/email/send"
|
||||||
|
EMAIL_LOGIN: str = "https://pmail.danding.vip/api/login"
|
||||||
|
EMAIL_USER: str = "admin"
|
||||||
|
EMAIL_FROM: str = "admin@danding.vip"
|
||||||
|
EMAIL_PASSWORD: str = "Grkwdc13"
|
||||||
155
danding_bot/plugins/danding_api/utils.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import requests
|
||||||
|
from nonebot import get_plugin_config
|
||||||
|
from .config import Config
|
||||||
|
from nonebot import logger
|
||||||
|
|
||||||
|
plugin_config = get_plugin_config(Config)
|
||||||
|
router:dict = {
|
||||||
|
"在线人数":"bot_online_count",
|
||||||
|
"添加卡密":"bot_add_kami",
|
||||||
|
"生成卡密":"bot_create_kami",
|
||||||
|
"用户加时":"bot_add_user_viptime",
|
||||||
|
"生成QQ验证码":"bot_generate_vcode",
|
||||||
|
"获取日志":"bot_get_user_log"
|
||||||
|
}
|
||||||
|
|
||||||
|
async def post(router_name:str,user:str,data:dict={})->str:
|
||||||
|
_url:str = plugin_config.DDApi_Host + router[router_name]
|
||||||
|
data["user"]=user
|
||||||
|
data["token"]=plugin_config.Token
|
||||||
|
r = requests.post(url = _url,json=data)
|
||||||
|
logger.debug(r)
|
||||||
|
if r.status_code != 200:
|
||||||
|
return '出错啦!'
|
||||||
|
r=r.json()
|
||||||
|
logger.debug(r)
|
||||||
|
return r["message"]
|
||||||
|
|
||||||
|
async def post_vcode(user:str)->str:
|
||||||
|
_url:str = plugin_config.DDApi_Host + router["生成QQ验证码"]
|
||||||
|
data:dict={}
|
||||||
|
data["user"]="1424473282"
|
||||||
|
data["token"]=plugin_config.Token
|
||||||
|
data["qq"]=user
|
||||||
|
r = requests.post(url = _url,json=data)
|
||||||
|
logger.debug(r)
|
||||||
|
if r.status_code != 200:
|
||||||
|
return '出错啦!'
|
||||||
|
r=r.json()
|
||||||
|
logger.debug(r)
|
||||||
|
if "验证码生成成功" in r["message"]:
|
||||||
|
resp_data = await send_mail(f'{user}@qq.com',"验证码生成成功",r["message"],"DanDing-Admin")
|
||||||
|
if resp_data is None or resp_data.get("errorNo", -1) != 0:
|
||||||
|
return r["message"]
|
||||||
|
else:
|
||||||
|
return f"生成的绑定验证码已经发送到 {user}@qq.com 邮箱中,请查收!"
|
||||||
|
return r["message"]
|
||||||
|
|
||||||
|
async def get_log(user:str)->str:
|
||||||
|
_url:str = plugin_config.DDApi_Host + router["获取日志"]
|
||||||
|
r = requests.get(url = f"{_url}?user=1424473282&token={plugin_config.Token}&qq={user}")
|
||||||
|
logger.debug(r)
|
||||||
|
if r.status_code != 200:
|
||||||
|
return '出错啦!'
|
||||||
|
r=r.json()
|
||||||
|
logger.debug(r)
|
||||||
|
return r["message"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_classes(classee:str):
|
||||||
|
"""
|
||||||
|
将口语类型转换为程序可识别的标准卡密类型
|
||||||
|
"""
|
||||||
|
cases = {
|
||||||
|
'day': 'Day',
|
||||||
|
'DAY': 'Day',
|
||||||
|
'天': 'Day',
|
||||||
|
'天卡': 'Day',
|
||||||
|
|
||||||
|
'week': 'Week',
|
||||||
|
'WEEK': 'Week',
|
||||||
|
'周': 'Week',
|
||||||
|
'周卡': 'Week',
|
||||||
|
|
||||||
|
'month': 'Month',
|
||||||
|
'MONTH': 'Month',
|
||||||
|
'月': 'Month',
|
||||||
|
'月卡': 'Month',
|
||||||
|
}
|
||||||
|
return cases.get(classee, '')
|
||||||
|
|
||||||
|
|
||||||
|
session_id: str = ""
|
||||||
|
# 登录pmail邮箱 获取cookie
|
||||||
|
login_url = plugin_config.EMAIL_LOGIN
|
||||||
|
login_pdata = {
|
||||||
|
"account": plugin_config.EMAIL_USER,
|
||||||
|
"password": plugin_config.EMAIL_PASSWORD
|
||||||
|
}
|
||||||
|
session = requests.session() # 实例化session对象
|
||||||
|
|
||||||
|
|
||||||
|
def login_pmail():
|
||||||
|
global session_id
|
||||||
|
resp_data = None
|
||||||
|
error_msg: str = ""
|
||||||
|
retries = 3 # 设置重试次数
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
resp_data = session.post(login_url, json=login_pdata, headers={'Content-Type': 'application/json'})
|
||||||
|
if resp_data.status_code == 200 and resp_data.json().get('errorNo') == 0:
|
||||||
|
logger.info('PMail App 启动成功!')
|
||||||
|
session_id = resp_data.headers['Set-Cookie']
|
||||||
|
return
|
||||||
|
except ConnectionError:
|
||||||
|
error_msg = "服务器连接失败!"
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
logger.warning(f'PMail App 登录失败,正在重试... ({attempt + 1}/{retries})')
|
||||||
|
|
||||||
|
# 如果重试次数用尽仍然失败
|
||||||
|
logger.error(f'PMail App 登录失败!无法使用邮件功能!可能网络错误!{error_msg}')
|
||||||
|
|
||||||
|
|
||||||
|
async def send_mail(mail_to, subject, content, name):
|
||||||
|
"""
|
||||||
|
发送邮件
|
||||||
|
:param mail_to: 发送到
|
||||||
|
:param subject: 标题
|
||||||
|
:param content: 内容
|
||||||
|
:param name: 用户名
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
url = plugin_config.EMAIL_API
|
||||||
|
|
||||||
|
pdata = {
|
||||||
|
'from':
|
||||||
|
{
|
||||||
|
"name": "DanDing-Admin",
|
||||||
|
"email": plugin_config.EMAIL_FROM
|
||||||
|
},
|
||||||
|
'to':
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"email": mail_to
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'subject': subject,
|
||||||
|
'html': content,
|
||||||
|
"text": "text"
|
||||||
|
}
|
||||||
|
if session_id is None or "":
|
||||||
|
logger.error("[error] 邮件发送失败,没有session_id,尝试重新登录邮箱服务!")
|
||||||
|
login_pmail()
|
||||||
|
|
||||||
|
resp_data = session.post(url, json=pdata, headers={"cookie": f"{session_id}"}).json()
|
||||||
|
if resp_data is None or resp_data.get("errorNo", -1) != 0:
|
||||||
|
logger.error("[error] 邮件发送失败,未知的错误,尝试重新登录邮箱服务!")
|
||||||
|
# 重新登录pmail邮箱
|
||||||
|
login_pmail()
|
||||||
|
resp_data = session.post(url, json=pdata, headers={"cookie": f"{session_id}"}).json()
|
||||||
|
if resp_data is None or resp_data.get("errorNo", -1) != 0:
|
||||||
|
return {"errorNo": 0, "errorMsg": "", "data": ""}
|
||||||
|
|
||||||
|
return resp_data
|
||||||
15
danding_bot/plugins/danding_help/HelpCaiDan.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 欢迎使用蛋定助手🎇
|
||||||
|
## 发送以下关键词获取帮助🤝🏻
|
||||||
|
1. 下载(dl)
|
||||||
|
2. 帮助文档(wd)
|
||||||
|
3. 公益版(gyb)
|
||||||
|
4. 正式版(zsb)
|
||||||
|
5. 下单(xd)
|
||||||
|
|
||||||
|
## 部分教程关键字✨️
|
||||||
|
1. 正式版御魂双开(dyh)
|
||||||
|
2. 正式版如何运行(htr)
|
||||||
|
|
||||||
|
## 活动🔥🔥
|
||||||
|
1. 每日试用(mrss) - 永久活动,单设备每日可试用1小时!
|
||||||
|
|
||||||
30
danding_bot/plugins/danding_help/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from nonebot import get_plugin_config
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from . import help
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="danding_help",
|
||||||
|
description="蛋定助手帮助信息",
|
||||||
|
usage="""
|
||||||
|
# 蛋定助手帮助系统
|
||||||
|
## 基础指令
|
||||||
|
- 帮助 / help:显示帮助菜单图片
|
||||||
|
- 下载 / dl / downdload:获取下载相关信息
|
||||||
|
- 帮助文档 / wd:获取在线帮助文档链接
|
||||||
|
|
||||||
|
## 版本信息
|
||||||
|
- 公益版 / free / gyb:查看公益版相关说明
|
||||||
|
- 正式版 / pro / zsb:查看正式版相关说明
|
||||||
|
|
||||||
|
## 使用教程
|
||||||
|
- 正式版御魂双开 / dyh:查看御魂双开教程
|
||||||
|
- 正式版如何运行 / htr:查看软件运行教程
|
||||||
|
- 正式版内测计划 / zsbnc:查看内测相关信息
|
||||||
|
|
||||||
|
注意:本插件仅在特定群组中可用
|
||||||
|
""",
|
||||||
|
config=Config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
45
danding_bot/plugins/danding_help/config.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
"""Plugin Config Here"""
|
||||||
|
HelpStr:str = """\
|
||||||
|
# 欢迎使用蛋定助手
|
||||||
|
## 发送以下关键词获取帮助
|
||||||
|
1. 下载
|
||||||
|
2. 下单
|
||||||
|
3. 公益版(暂未进行维护!)
|
||||||
|
4. 正式版
|
||||||
|
|
||||||
|
## 部分教程关键字
|
||||||
|
1. 正式版御魂双开
|
||||||
|
2. 正式版如何运行
|
||||||
|
|
||||||
|
## 活动
|
||||||
|
1. 每日试用"""
|
||||||
|
|
||||||
|
DowndLoadStr:str = """\
|
||||||
|
公益版提供群文件下载一种方式;
|
||||||
|
正式版下载:https://file.x-tools.top
|
||||||
|
后缀为Cx结尾的版本是修正版本,用新不用旧;"""
|
||||||
|
|
||||||
|
FreeStr:str = """公益版(暂时没有精力维护)
|
||||||
|
1. 不定时更新,仅维护现有功能;
|
||||||
|
2. 不提供技术支持;
|
||||||
|
3. 仅供群文件下载;
|
||||||
|
若出现初始化闪退问题,请@开发者更新共享注册码;"""
|
||||||
|
|
||||||
|
ProStr:str = """正式版
|
||||||
|
1. 全新的框架、全新UI、更稳的功能;
|
||||||
|
2. 更新、维护频率快;
|
||||||
|
3. 提供技术支持;
|
||||||
|
4. 提供多种下载方式;"""
|
||||||
|
|
||||||
|
OrderStr:str = """\
|
||||||
|
1. 蛋定の卡铺1:https://shop.danding.vip
|
||||||
|
2. 蛋定の卡铺2:https://ka.x-tools.top
|
||||||
|
"""
|
||||||
|
|
||||||
|
DailyTrialStr:str = """\
|
||||||
|
永久活动-每日试用:单设备支持每日试用1小时的脚本时长!
|
||||||
|
请在购买或支持蛋定助手前,一定要先试用,确保自己可以正常使用脚本!"""
|
||||||
99
danding_bot/plugins/danding_help/help.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
from nonebot import on_command, get_plugin_config,logger
|
||||||
|
from nonebot.rule import fullmatch
|
||||||
|
from .config import Config
|
||||||
|
import os
|
||||||
|
from nonebot_plugin_saa import Text, Image, MessageFactory
|
||||||
|
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
|
||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def rule_fun(e:GroupMessageEvent):
|
||||||
|
id = e.group_id
|
||||||
|
if id in [621016172]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
plugin_config = get_plugin_config(Config)
|
||||||
|
|
||||||
|
help = on_command("帮助", rule=rule_fun and fullmatch(('帮助','help')), aliases={"help"}, priority=1, block=True)
|
||||||
|
@help.handle()
|
||||||
|
async def _():
|
||||||
|
# 获取当前脚本所在目录的绝对路径
|
||||||
|
current_dir = os.path.dirname(__file__)
|
||||||
|
# 构造图片的绝对路径
|
||||||
|
image_path = os.path.join(current_dir, "img", "帮助菜单.jpg")
|
||||||
|
# 发送图片
|
||||||
|
with open(image_path, "rb") as f:
|
||||||
|
image_bytes = f.read()
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await MessageFactory([Image(image_bytes)]).send(
|
||||||
|
at_sender=True, reply=True
|
||||||
|
)
|
||||||
|
|
||||||
|
downdload = on_command("下载", rule=rule_fun and fullmatch(('下载',"dl","downdload")), aliases={"dl","downdload"}, priority=1, block=True)
|
||||||
|
@downdload.handle()
|
||||||
|
async def _():
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await downdload.finish(plugin_config.DowndLoadStr)
|
||||||
|
|
||||||
|
wd = on_command("帮助文档", rule=rule_fun and fullmatch(('帮助文档',"wd")), aliases={"wd"}, priority=1, block=True)
|
||||||
|
@wd.handle()
|
||||||
|
async def _():
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await wd.finish("https://www.danding.vip")
|
||||||
|
|
||||||
|
|
||||||
|
free = on_command("公益版", rule=rule_fun and fullmatch(('公益版',"free","gyb")), aliases={"free","gyb"}, priority=1, block=True)
|
||||||
|
@free.handle()
|
||||||
|
async def _():
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await help.finish(plugin_config.FreeStr)
|
||||||
|
|
||||||
|
pro = on_command("正式版", rule=rule_fun and fullmatch(('正式版',"pro","zsb")), aliases={"pro","zsb"}, priority=1, block=True)
|
||||||
|
@pro.handle()
|
||||||
|
async def _():
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await help.finish(plugin_config.ProStr)
|
||||||
|
|
||||||
|
dyh = on_command("正式版御魂双开", rule=rule_fun and fullmatch(('正式版御魂双开',"dyh")), aliases={"dyh"}, priority=1, block=True)
|
||||||
|
@dyh.handle()
|
||||||
|
async def _():
|
||||||
|
# 获取当前脚本所在目录的绝对路径
|
||||||
|
current_dir = os.path.dirname(__file__)
|
||||||
|
# 构造图片的绝对路径
|
||||||
|
image_path = os.path.join(current_dir, "img", "御魂双开方法.jpg")
|
||||||
|
# 发送图片
|
||||||
|
with open(image_path, "rb") as f:
|
||||||
|
image_bytes = f.read()
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await MessageFactory([Text("御魂双开方法见下图"),Image(image_bytes)]).send(
|
||||||
|
at_sender=True, reply=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
htr = on_command("正式版如何运行", rule=rule_fun and fullmatch(('正式版如何运行',"htr")), aliases={"htr"}, priority=1, block=True)
|
||||||
|
@htr.handle()
|
||||||
|
async def _():
|
||||||
|
# 获取当前脚本所在目录的绝对路径
|
||||||
|
current_dir = os.path.dirname(__file__)
|
||||||
|
# 构造图片的绝对路径
|
||||||
|
image_path = os.path.join(current_dir, "img", "开软件教程.jpg")
|
||||||
|
# 发送图片
|
||||||
|
with open(image_path, "rb") as f:
|
||||||
|
image_bytes = f.read()
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await MessageFactory([Text("How To Run? Look!"),Image(image_bytes)]).send(
|
||||||
|
at_sender=True, reply=True
|
||||||
|
)
|
||||||
|
|
||||||
|
order = on_command("下单", rule=rule_fun and fullmatch(('下单',"xd")), aliases={"xd"}, priority=1, block=True)
|
||||||
|
@order.handle()
|
||||||
|
async def _():
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await order.finish(plugin_config.OrderStr)
|
||||||
|
|
||||||
|
daily_trial = on_command("每日试用", rule=rule_fun and fullmatch(('每日试用',"mrss")), aliases={"mrss"}, priority=1, block=True)
|
||||||
|
@daily_trial.handle()
|
||||||
|
async def _():
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
await daily_trial.finish(plugin_config.DailyTrialStr)
|
||||||
BIN
danding_bot/plugins/danding_help/img/帮助菜单.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
danding_bot/plugins/danding_help/img/开软件教程.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
danding_bot/plugins/danding_help/img/御魂双开方法.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
danding_bot/plugins/onmyoji_gacha.zip
Normal file
771
danding_bot/plugins/onmyoji_gacha/__init__.py
Normal file
@@ -0,0 +1,771 @@
|
|||||||
|
import os
|
||||||
|
from nonebot import on_command, on_startswith
|
||||||
|
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent, Message
|
||||||
|
from nonebot.adapters.onebot.v11.message import MessageSegment
|
||||||
|
from nonebot.typing import T_State
|
||||||
|
from nonebot.rule import Rule
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .gacha import GachaSystem
|
||||||
|
from .utils import format_user_mention, get_image_path
|
||||||
|
from .api_utils import process_ssr_sp_reward, process_achievement_reward
|
||||||
|
from . import web_api
|
||||||
|
|
||||||
|
# 创建Config实例
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
# 允许的群聊ID和用户ID
|
||||||
|
ALLOWED_GROUP_ID = config.ALLOWED_GROUP_ID
|
||||||
|
ALLOWED_USER_ID = config.ALLOWED_USER_ID
|
||||||
|
GACHA_COMMANDS = config.GACHA_COMMANDS
|
||||||
|
STATS_COMMANDS = config.STATS_COMMANDS
|
||||||
|
DAILY_STATS_COMMANDS = config.DAILY_STATS_COMMANDS
|
||||||
|
TRIPLE_GACHA_COMMANDS = config.TRIPLE_GACHA_COMMANDS
|
||||||
|
ACHIEVEMENT_COMMANDS = config.ACHIEVEMENT_COMMANDS
|
||||||
|
INTRO_COMMANDS = config.INTRO_COMMANDS
|
||||||
|
DAILY_LIMIT = config.DAILY_LIMIT
|
||||||
|
|
||||||
|
gacha_system = GachaSystem()
|
||||||
|
|
||||||
|
# 检查是否允许使用功能的规则
|
||||||
|
def check_permission() -> Rule:
|
||||||
|
async def _checker(event: MessageEvent) -> bool:
|
||||||
|
# 允许特定用户在任何场景下使用
|
||||||
|
if event.user_id == ALLOWED_USER_ID:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 在允许的群聊中任何人都可以使用
|
||||||
|
if isinstance(event, GroupMessageEvent) and event.group_id == ALLOWED_GROUP_ID:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
return Rule(_checker)
|
||||||
|
|
||||||
|
# 注册抽卡命令,添加权限检查规则
|
||||||
|
gacha_matcher = on_command("抽卡", aliases=set(GACHA_COMMANDS), priority=10, rule=check_permission())
|
||||||
|
|
||||||
|
@gacha_matcher.handle()
|
||||||
|
async def handle_gacha(bot: Bot, event: MessageEvent, state: T_State):
|
||||||
|
user_id = str(event.user_id)
|
||||||
|
user_name = event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname
|
||||||
|
|
||||||
|
# 执行抽卡
|
||||||
|
result = gacha_system.draw(user_id)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
await gacha_matcher.finish(format_user_mention(user_id, user_name) + " ❌ " + result["message"])
|
||||||
|
|
||||||
|
# 成功抽卡,格式化消息
|
||||||
|
rarity = result["rarity"]
|
||||||
|
name = result["name"]
|
||||||
|
image_url = result["image_url"]
|
||||||
|
draws_left = result["draws_left"]
|
||||||
|
unlocked_achievements = result.get("unlocked_achievements", [])
|
||||||
|
|
||||||
|
# 构建消息
|
||||||
|
msg = Message()
|
||||||
|
|
||||||
|
# 根据稀有度设置不同的消息样式
|
||||||
|
if rarity == "SSR":
|
||||||
|
msg.append(f"🌟✨ 恭喜 {format_user_mention(user_id, user_name)} ✨🌟\n")
|
||||||
|
msg.append(f"🎊 抽到了 SSR 式神:{name} 🎊\n")
|
||||||
|
msg.append(f"💫 真是太幸运了!💫")
|
||||||
|
elif rarity == "SP":
|
||||||
|
msg.append(f"🌈🎆 恭喜 {format_user_mention(user_id, user_name)} 🎆🌈\n")
|
||||||
|
msg.append(f"🎉 抽到了 SP 式神:{name} 🎉\n")
|
||||||
|
msg.append(f"🔥 这是传说中的SP!🔥")
|
||||||
|
elif rarity == "SR":
|
||||||
|
msg.append(f"⭐ 恭喜 {format_user_mention(user_id, user_name)} ⭐\n")
|
||||||
|
msg.append(f"✨ 抽到了 SR 式神:{name} ✨")
|
||||||
|
else: # R
|
||||||
|
msg.append(f"🍀 {format_user_mention(user_id, user_name)} 🍀\n")
|
||||||
|
msg.append(f"📜 抽到了 R 式神:{name}")
|
||||||
|
|
||||||
|
# 添加图片
|
||||||
|
if image_url and os.path.exists(image_url):
|
||||||
|
msg.append(MessageSegment.image(f"file:///{get_image_path(image_url)}"))
|
||||||
|
|
||||||
|
# 添加成就通知
|
||||||
|
if unlocked_achievements:
|
||||||
|
msg.append("\n\n🏆 恭喜解锁新成就!\n")
|
||||||
|
has_manual_rewards = False
|
||||||
|
|
||||||
|
for achievement_id in unlocked_achievements:
|
||||||
|
# 尝试自动发放成就奖励
|
||||||
|
auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id)
|
||||||
|
|
||||||
|
# 检查是否是重复奖励
|
||||||
|
if "_repeat_" in achievement_id:
|
||||||
|
base_achievement_id = achievement_id.split("_repeat_")[0]
|
||||||
|
achievement_config = config.ACHIEVEMENTS.get(base_achievement_id)
|
||||||
|
if achievement_config:
|
||||||
|
achievement_name = achievement_config["name"]
|
||||||
|
# 使用重复奖励或默认为天卡
|
||||||
|
reward = achievement_config.get("repeat_reward", "天卡")
|
||||||
|
status = "✅ 已自动发放" if auto_success else "⚠️ 需手动领取"
|
||||||
|
msg.append(f"🎖️ {achievement_name} 重复奖励 (奖励:{reward}) {status}\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"🎖️ {achievement_id}\n")
|
||||||
|
else:
|
||||||
|
achievement_config = config.ACHIEVEMENTS.get(achievement_id)
|
||||||
|
if achievement_config:
|
||||||
|
achievement_name = achievement_config["name"]
|
||||||
|
reward = achievement_config["reward"]
|
||||||
|
status = "✅ 已自动发放" if auto_success else "⚠️ 需手动领取"
|
||||||
|
msg.append(f"🎖️ {achievement_name} (奖励:{reward}) {status}\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"🎖️ {achievement_id}\n")
|
||||||
|
|
||||||
|
# 记录是否有需要手动领取的奖励
|
||||||
|
if not auto_success:
|
||||||
|
has_manual_rewards = True
|
||||||
|
|
||||||
|
# 如果有未自动发放的奖励,提示联系管理员
|
||||||
|
if has_manual_rewards:
|
||||||
|
msg.append("💰 未自动发放的奖励请联系管理员\n")
|
||||||
|
|
||||||
|
# 添加成就进度提示
|
||||||
|
achievement_data = gacha_system.get_user_achievements(user_id)
|
||||||
|
if achievement_data["success"]:
|
||||||
|
progress = achievement_data["progress"]
|
||||||
|
consecutive_days = progress.get("consecutive_days", 0)
|
||||||
|
no_ssr_streak = progress.get("no_ssr_streak", 0)
|
||||||
|
|
||||||
|
msg.append("\n📈 成就进度:\n")
|
||||||
|
|
||||||
|
# 连续抽卡天数进度
|
||||||
|
if consecutive_days > 0:
|
||||||
|
if consecutive_days < 30:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅰ:{consecutive_days}/30天 🎯\n")
|
||||||
|
elif consecutive_days < 60:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅱ:{consecutive_days}/60天 🎯\n")
|
||||||
|
elif consecutive_days < 90:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅲ:{consecutive_days}/90天 🎯\n")
|
||||||
|
elif consecutive_days < 120:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅳ:{consecutive_days}/120天 ⭐\n")
|
||||||
|
elif consecutive_days < 150:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅴ:{consecutive_days}/150天 ⭐\n")
|
||||||
|
else:
|
||||||
|
next_reward_days = 30 - (consecutive_days % 30)
|
||||||
|
if next_reward_days == 30:
|
||||||
|
next_reward_days = 0
|
||||||
|
if next_reward_days > 0:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅴ (满级):{consecutive_days}天,距离下次奖励{next_reward_days}天 🎁\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅴ (满级):{consecutive_days}天,可获得奖励!🎉\n")
|
||||||
|
|
||||||
|
# 无SSR/SP连击进度
|
||||||
|
if no_ssr_streak > 0:
|
||||||
|
if no_ssr_streak < 60:
|
||||||
|
msg.append(f"💔 非酋进度:{no_ssr_streak}/60次 😭\n")
|
||||||
|
elif no_ssr_streak < 120:
|
||||||
|
msg.append(f"💔 顶级非酋:{no_ssr_streak}/120次 😱\n")
|
||||||
|
elif no_ssr_streak < 180:
|
||||||
|
msg.append(f"💔 月见黑:{no_ssr_streak}/180次 🌙\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"💔 已达月见黑级别:{no_ssr_streak}次 🌚\n")
|
||||||
|
|
||||||
|
# 添加剩余次数和概率信息
|
||||||
|
msg.append(f"\n📊 今日剩余抽卡次数:{draws_left}/{DAILY_LIMIT}\n")
|
||||||
|
msg.append(gacha_system.get_probability_text())
|
||||||
|
|
||||||
|
# 如果抽到了SSR或SP,处理奖励发放
|
||||||
|
if rarity in ["SSR", "SP"]:
|
||||||
|
# 尝试自动发放奖励
|
||||||
|
auto_success, reward_msg = await process_ssr_sp_reward(user_id)
|
||||||
|
msg.append(f"\n\n{reward_msg}")
|
||||||
|
|
||||||
|
# 通知管理员好友
|
||||||
|
admin_id = 2185330092
|
||||||
|
notify_msg = Message()
|
||||||
|
if auto_success:
|
||||||
|
notify_msg.append(f"用户 {user_name}({user_id}) 抽中了{rarity}稀有度式神: {name}! 已自动发放奖励!")
|
||||||
|
else:
|
||||||
|
notify_msg.append(f"用户 {user_name}({user_id}) 抽中了{rarity}稀有度式神: {name}! 需要手动发放奖励!")
|
||||||
|
await bot.send_private_msg(user_id=admin_id, message=notify_msg)
|
||||||
|
else:
|
||||||
|
msg.append(f"\n\n抽中SSR或SP时,可获得蛋定助手天卡一张哦~~")
|
||||||
|
|
||||||
|
await gacha_matcher.finish(msg)
|
||||||
|
|
||||||
|
async def notify_admin(bot: Bot, message: str):
|
||||||
|
"""通知管理员"""
|
||||||
|
admin_id = 2185330092
|
||||||
|
try:
|
||||||
|
await bot.send_private_msg(user_id=admin_id, message=message)
|
||||||
|
except Exception as e:
|
||||||
|
pass # 忽略通知失败的错误
|
||||||
|
|
||||||
|
# 注册查询命令,添加权限检查规则
|
||||||
|
stats_matcher = on_command("我的抽卡", aliases=set(STATS_COMMANDS), priority=5, rule=check_permission())
|
||||||
|
|
||||||
|
# 注册今日统计命令
|
||||||
|
daily_stats_matcher = on_command("今日抽卡", aliases=set(DAILY_STATS_COMMANDS), priority=5, rule=check_permission())
|
||||||
|
|
||||||
|
# 注册三连抽命令
|
||||||
|
triple_gacha_matcher = on_command("三连抽", aliases=set(TRIPLE_GACHA_COMMANDS), priority=5, rule=check_permission())
|
||||||
|
|
||||||
|
# 注册成就查询命令
|
||||||
|
achievement_matcher = on_command("查询成就", aliases=set(ACHIEVEMENT_COMMANDS), priority=5, rule=check_permission())
|
||||||
|
|
||||||
|
@stats_matcher.handle()
|
||||||
|
async def handle_stats(bot: Bot, event: MessageEvent, state: T_State):
|
||||||
|
user_id = str(event.user_id)
|
||||||
|
user_name = event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname
|
||||||
|
|
||||||
|
# 获取用户统计
|
||||||
|
stats = gacha_system.get_user_stats(user_id)
|
||||||
|
|
||||||
|
if not stats["success"]:
|
||||||
|
await stats_matcher.finish(format_user_mention(user_id, user_name) + " " + stats["message"])
|
||||||
|
|
||||||
|
# 构建消息
|
||||||
|
msg = Message()
|
||||||
|
msg.append(f"📊 {format_user_mention(user_id, user_name)} 的抽卡统计:\n\n")
|
||||||
|
msg.append(f"🎲 总抽卡次数:{stats['total_draws']}次\n\n")
|
||||||
|
|
||||||
|
# 稀有度统计
|
||||||
|
msg.append("🎯 稀有度分布:\n")
|
||||||
|
msg.append(f"📜 R:{stats['R_count']}张 ({stats['R_count']/stats['total_draws']*100:.1f}%)\n")
|
||||||
|
msg.append(f"⭐ SR:{stats['SR_count']}张 ({stats['SR_count']/stats['total_draws']*100:.1f}%)\n")
|
||||||
|
msg.append(f"🌟 SSR:{stats['SSR_count']}张 ({stats['SSR_count']/stats['total_draws']*100:.1f}%)\n")
|
||||||
|
msg.append(f"🌈 SP:{stats['SP_count']}张 ({stats['SP_count']/stats['total_draws']*100:.1f}%)\n")
|
||||||
|
|
||||||
|
# 添加最近抽卡记录
|
||||||
|
if stats["recent_draws"]:
|
||||||
|
msg.append("\n🕐 最近抽卡记录:\n")
|
||||||
|
for draw in reversed(stats["recent_draws"]):
|
||||||
|
# 根据稀有度添加emoji
|
||||||
|
if draw['rarity'] == "SSR":
|
||||||
|
emoji = "🌟"
|
||||||
|
elif draw['rarity'] == "SP":
|
||||||
|
emoji = "🌈"
|
||||||
|
elif draw['rarity'] == "SR":
|
||||||
|
emoji = "⭐"
|
||||||
|
else:
|
||||||
|
emoji = "📜"
|
||||||
|
|
||||||
|
msg.append(f"{emoji} {draw['rarity']} - {draw['name']} ({draw['date']})\n")
|
||||||
|
|
||||||
|
await stats_matcher.finish(msg)
|
||||||
|
|
||||||
|
@triple_gacha_matcher.handle()
|
||||||
|
async def handle_triple_gacha(bot: Bot, event: MessageEvent, state: T_State):
|
||||||
|
"""处理三连抽命令"""
|
||||||
|
user_id = str(event.user_id)
|
||||||
|
user_name = event.sender.card or event.sender.nickname or "未知用户"
|
||||||
|
|
||||||
|
# 执行三连抽
|
||||||
|
result = gacha_system.triple_draw(user_id)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
await triple_gacha_matcher.finish(f"❌ {result['message']}")
|
||||||
|
|
||||||
|
# 构建三连抽结果消息
|
||||||
|
msg = Message()
|
||||||
|
msg.append(f"🎯 {format_user_mention(user_id, user_name)} 的三连抽结果:\n\n")
|
||||||
|
|
||||||
|
# 显示每次抽卡结果
|
||||||
|
for i, draw_result in enumerate(result["results"], 1):
|
||||||
|
rarity = draw_result["rarity"]
|
||||||
|
name = draw_result["name"]
|
||||||
|
|
||||||
|
# 根据稀有度添加emoji
|
||||||
|
if rarity == "SSR":
|
||||||
|
msg.append(f"🌟 第{i}抽:SSR - {name}\n")
|
||||||
|
elif rarity == "SP":
|
||||||
|
msg.append(f"🌈 第{i}抽:SP - {name}\n")
|
||||||
|
elif rarity == "SR":
|
||||||
|
msg.append(f"⭐ 第{i}抽:SR - {name}\n")
|
||||||
|
else: # R
|
||||||
|
msg.append(f"📜 第{i}抽:R - {name}\n")
|
||||||
|
|
||||||
|
# 统计结果
|
||||||
|
ssr_count = sum(1 for r in result["results"] if r["rarity"] in ["SSR", "SP"])
|
||||||
|
sr_count = sum(1 for r in result["results"] if r["rarity"] == "SR")
|
||||||
|
r_count = sum(1 for r in result["results"] if r["rarity"] == "R")
|
||||||
|
|
||||||
|
msg.append(f"\n📈 本次三连抽统计:\n")
|
||||||
|
if ssr_count > 0:
|
||||||
|
msg.append(f"🎊 SSR/SP:{ssr_count}张\n")
|
||||||
|
if sr_count > 0:
|
||||||
|
msg.append(f"✨ SR:{sr_count}张\n")
|
||||||
|
if r_count > 0:
|
||||||
|
msg.append(f"📜 R:{r_count}张\n")
|
||||||
|
|
||||||
|
# 添加成就通知
|
||||||
|
unlocked_achievements = result.get("unlocked_achievements", [])
|
||||||
|
if unlocked_achievements:
|
||||||
|
msg.append("\n🏆 恭喜解锁新成就!\n")
|
||||||
|
has_manual_rewards = False
|
||||||
|
|
||||||
|
for achievement_id in unlocked_achievements:
|
||||||
|
# 尝试自动发放成就奖励
|
||||||
|
auto_success, reward_msg = await process_achievement_reward(user_id, achievement_id)
|
||||||
|
|
||||||
|
# 检查是否是重复奖励
|
||||||
|
if "_repeat_" in achievement_id:
|
||||||
|
base_achievement_id = achievement_id.split("_repeat_")[0]
|
||||||
|
achievement_config = config.ACHIEVEMENTS.get(base_achievement_id)
|
||||||
|
if achievement_config:
|
||||||
|
achievement_name = achievement_config["name"]
|
||||||
|
# 使用重复奖励或默认为天卡
|
||||||
|
reward = achievement_config.get("repeat_reward", "天卡")
|
||||||
|
status = "✅ 已自动发放" if auto_success else "⚠️ 需手动领取"
|
||||||
|
msg.append(f"🎖️ {achievement_name} 重复奖励 (奖励:{reward}) {status}\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"🎖️ {achievement_id}\n")
|
||||||
|
else:
|
||||||
|
achievement_config = config.ACHIEVEMENTS.get(achievement_id)
|
||||||
|
if achievement_config:
|
||||||
|
achievement_name = achievement_config["name"]
|
||||||
|
reward = achievement_config["reward"]
|
||||||
|
status = "✅ 已自动发放" if auto_success else "⚠️ 需手动领取"
|
||||||
|
msg.append(f"🎖️ {achievement_name} (奖励:{reward}) {status}\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"🎖️ {achievement_id}\n")
|
||||||
|
|
||||||
|
# 记录是否有需要手动领取的奖励
|
||||||
|
if not auto_success:
|
||||||
|
has_manual_rewards = True
|
||||||
|
|
||||||
|
# 如果有未自动发放的奖励,提示联系管理员
|
||||||
|
if has_manual_rewards:
|
||||||
|
msg.append("💰 未自动发放的奖励请联系管理员\n")
|
||||||
|
|
||||||
|
# 添加成就进度提示
|
||||||
|
achievement_data = gacha_system.get_user_achievements(user_id)
|
||||||
|
if achievement_data["success"]:
|
||||||
|
progress = achievement_data["progress"]
|
||||||
|
consecutive_days = progress.get("consecutive_days", 0)
|
||||||
|
no_ssr_streak = progress.get("no_ssr_streak", 0)
|
||||||
|
|
||||||
|
msg.append("\n📈 成就进度:\n")
|
||||||
|
|
||||||
|
# 连续抽卡天数进度
|
||||||
|
if consecutive_days > 0:
|
||||||
|
if consecutive_days < 30:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅰ:{consecutive_days}/30天 🎯\n")
|
||||||
|
elif consecutive_days < 60:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅱ:{consecutive_days}/60天 🎯\n")
|
||||||
|
elif consecutive_days < 90:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅲ:{consecutive_days}/90天 🎯\n")
|
||||||
|
elif consecutive_days < 120:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅳ:{consecutive_days}/120天 ⭐\n")
|
||||||
|
elif consecutive_days < 150:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅴ:{consecutive_days}/150天 ⭐\n")
|
||||||
|
else:
|
||||||
|
next_reward_days = 30 - (consecutive_days % 30)
|
||||||
|
if next_reward_days == 30:
|
||||||
|
next_reward_days = 0
|
||||||
|
if next_reward_days > 0:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅴ (满级):{consecutive_days}天,距离下次奖励{next_reward_days}天 🎁\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅴ (满级):{consecutive_days}天,可获得奖励!🎉\n")
|
||||||
|
|
||||||
|
# 无SSR/SP连击进度
|
||||||
|
if no_ssr_streak > 0:
|
||||||
|
if no_ssr_streak < 60:
|
||||||
|
msg.append(f"💔 非酋进度:{no_ssr_streak}/60次 😭\n")
|
||||||
|
elif no_ssr_streak < 120:
|
||||||
|
msg.append(f"💔 顶级非酋:{no_ssr_streak}/120次 😱\n")
|
||||||
|
elif no_ssr_streak < 180:
|
||||||
|
msg.append(f"💔 月见黑:{no_ssr_streak}/180次 🌙\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"💔 已达月见黑级别:{no_ssr_streak}次 🌚\n")
|
||||||
|
|
||||||
|
# 添加剩余次数
|
||||||
|
draws_left = result["draws_left"]
|
||||||
|
msg.append(f"\n📊 今日剩余抽卡次数:{draws_left}/{DAILY_LIMIT}")
|
||||||
|
|
||||||
|
# 如果抽到SSR/SP,处理奖励发放
|
||||||
|
if ssr_count > 0:
|
||||||
|
# 为每张SSR/SP处理奖励
|
||||||
|
auto_rewards = 0
|
||||||
|
manual_rewards = 0
|
||||||
|
|
||||||
|
# 这里简化处理,只处理一次奖励(因为每次抽卡都是独立的)
|
||||||
|
# 如果需要为每张SSR/SP都发放奖励,可以循环处理
|
||||||
|
auto_success, reward_msg = await process_ssr_sp_reward(user_id)
|
||||||
|
if auto_success:
|
||||||
|
auto_rewards += 1
|
||||||
|
else:
|
||||||
|
manual_rewards += 1
|
||||||
|
|
||||||
|
msg.append(f"\n\n{reward_msg}")
|
||||||
|
|
||||||
|
# 通知管理员
|
||||||
|
admin_msg = f"🎉 用户 {user_name}({user_id}) 在三连抽中抽到了 {ssr_count} 张 SSR/SP!"
|
||||||
|
if auto_rewards > 0:
|
||||||
|
admin_msg += f" 已自动发放 {auto_rewards} 张奖励!"
|
||||||
|
if manual_rewards > 0:
|
||||||
|
admin_msg += f" 需要手动发放 {manual_rewards} 张奖励!"
|
||||||
|
await notify_admin(bot, admin_msg)
|
||||||
|
|
||||||
|
await triple_gacha_matcher.finish(msg)
|
||||||
|
|
||||||
|
@achievement_matcher.handle()
|
||||||
|
async def handle_achievement(bot: Bot, event: MessageEvent, state: T_State):
|
||||||
|
"""处理成就查询命令"""
|
||||||
|
user_id = str(event.user_id)
|
||||||
|
user_name = event.sender.card or event.sender.nickname or "未知用户"
|
||||||
|
|
||||||
|
# 获取用户成就信息
|
||||||
|
result = gacha_system.get_user_achievements(user_id)
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
await achievement_matcher.finish(f"❌ {result['message']}")
|
||||||
|
|
||||||
|
# 构建成就消息
|
||||||
|
msg = Message()
|
||||||
|
msg.append(f"🏆 {format_user_mention(user_id, user_name)} 的成就信息:\n\n")
|
||||||
|
|
||||||
|
# 显示已解锁成就
|
||||||
|
unlocked = result["achievements"]
|
||||||
|
if unlocked:
|
||||||
|
msg.append("🎖️ 已解锁成就:\n")
|
||||||
|
for achievement in unlocked:
|
||||||
|
# 检查是否是重复奖励
|
||||||
|
if "_repeat_" in achievement:
|
||||||
|
base_achievement_id = achievement.split("_repeat_")[0]
|
||||||
|
achievement_config = config.ACHIEVEMENTS.get(base_achievement_id)
|
||||||
|
if achievement_config:
|
||||||
|
achievement_name = achievement_config["name"]
|
||||||
|
reward = achievement_config.get("repeat_reward", "天卡")
|
||||||
|
msg.append(f"✅ {achievement_name} 重复奖励 (奖励:{reward})\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"✅ {achievement}\n")
|
||||||
|
else:
|
||||||
|
achievement_config = config.ACHIEVEMENTS.get(achievement)
|
||||||
|
if achievement_config:
|
||||||
|
achievement_name = achievement_config["name"]
|
||||||
|
reward = achievement_config["reward"]
|
||||||
|
msg.append(f"✅ {achievement_name} (奖励:{reward})\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"✅ {achievement}\n")
|
||||||
|
msg.append("\n💰 获取奖励请联系管理员\n\n")
|
||||||
|
|
||||||
|
# 显示成就进度
|
||||||
|
progress = result["progress"]
|
||||||
|
msg.append("📊 成就进度:\n")
|
||||||
|
|
||||||
|
# 连续抽卡天数 - 勤勤恳恳系列成就
|
||||||
|
consecutive_days = progress.get("consecutive_days", 0)
|
||||||
|
if consecutive_days > 0:
|
||||||
|
# 判断当前应该显示哪个等级的进度
|
||||||
|
if consecutive_days < 30:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅰ:{consecutive_days}/30 天 (奖励:天卡)\n")
|
||||||
|
elif consecutive_days < 60:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅱ:{consecutive_days}/60 天 (奖励:天卡)\n")
|
||||||
|
elif consecutive_days < 90:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅲ:{consecutive_days}/90 天 (奖励:天卡)\n")
|
||||||
|
elif consecutive_days < 120:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅳ:{consecutive_days}/120 天 (奖励:周卡)\n")
|
||||||
|
elif consecutive_days < 150:
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅴ:{consecutive_days}/150 天 (奖励:周卡)\n")
|
||||||
|
else:
|
||||||
|
# 已达到最高等级,显示下次奖励进度
|
||||||
|
next_reward_days = 30 - (consecutive_days % 30)
|
||||||
|
if next_reward_days == 30:
|
||||||
|
next_reward_days = 0
|
||||||
|
msg.append(f"📅 勤勤恳恳Ⅴ (已满级):{consecutive_days} 天\n")
|
||||||
|
if next_reward_days > 0:
|
||||||
|
msg.append(f"🎁 距离下次奖励:{next_reward_days} 天 (奖励:天卡)\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"🎁 可获得奖励!请联系管理员 (奖励:天卡)\n")
|
||||||
|
|
||||||
|
# 无SSR/SP连击数
|
||||||
|
no_ssr_streak = progress.get("no_ssr_streak", 0)
|
||||||
|
if no_ssr_streak > 0:
|
||||||
|
msg.append(f"💔 无SSR/SP连击:{no_ssr_streak} 次\n")
|
||||||
|
|
||||||
|
# 显示各个非酋成就的进度
|
||||||
|
if no_ssr_streak < 60:
|
||||||
|
msg.append(f" 🎯 非酋成就:{no_ssr_streak}/60 (奖励:天卡)\n")
|
||||||
|
elif no_ssr_streak < 120:
|
||||||
|
msg.append(f" 🎯 顶级非酋成就:{no_ssr_streak}/120 (奖励:周卡)\n")
|
||||||
|
elif no_ssr_streak < 180:
|
||||||
|
msg.append(f" 🎯 月见黑成就:{no_ssr_streak}/180 (奖励:月卡)\n")
|
||||||
|
else:
|
||||||
|
msg.append(f" 🌙 已达到月见黑级别!\n")
|
||||||
|
|
||||||
|
# 如果没有任何进度,显示提示
|
||||||
|
if consecutive_days == 0 and no_ssr_streak == 0:
|
||||||
|
msg.append("🌱 还没有任何成就进度,快去抽卡吧!")
|
||||||
|
|
||||||
|
await achievement_matcher.finish(msg)
|
||||||
|
|
||||||
|
# 注册查询抽卡指令,支持@用户查询功能
|
||||||
|
query_matcher = on_command("查询抽卡", aliases={"查询抽奖"}, priority=5, rule=check_permission())
|
||||||
|
|
||||||
|
@query_matcher.handle()
|
||||||
|
async def handle_query(bot: Bot, event: MessageEvent, state: T_State):
|
||||||
|
# 获取消息中的@用户
|
||||||
|
message = event.get_message()
|
||||||
|
at_segment = None
|
||||||
|
|
||||||
|
for segment in message:
|
||||||
|
if segment.type == "at":
|
||||||
|
at_segment = segment
|
||||||
|
break
|
||||||
|
|
||||||
|
# 确定查询的用户ID
|
||||||
|
if at_segment:
|
||||||
|
# 查询被@的用户
|
||||||
|
target_user_id = str(at_segment.data.get("qq", ""))
|
||||||
|
# 获取被@用户的信息
|
||||||
|
if isinstance(event, GroupMessageEvent):
|
||||||
|
try:
|
||||||
|
group_id = event.group_id
|
||||||
|
user_info = await bot.get_group_member_info(group_id=group_id, user_id=int(target_user_id))
|
||||||
|
target_user_name = user_info.get("card") or user_info.get("nickname", "用户")
|
||||||
|
except:
|
||||||
|
target_user_name = "用户"
|
||||||
|
else:
|
||||||
|
target_user_name = "用户"
|
||||||
|
else:
|
||||||
|
# 查询自己
|
||||||
|
target_user_id = str(event.user_id)
|
||||||
|
target_user_name = event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname
|
||||||
|
|
||||||
|
# 获取用户统计
|
||||||
|
stats = gacha_system.get_user_stats(target_user_id)
|
||||||
|
|
||||||
|
# 构建响应消息
|
||||||
|
msg = Message()
|
||||||
|
|
||||||
|
# 如果查询的是他人
|
||||||
|
if target_user_id != str(event.user_id):
|
||||||
|
msg.append(format_user_mention(str(event.user_id), event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname))
|
||||||
|
msg.append(f" 查询了 ")
|
||||||
|
msg.append(format_user_mention(target_user_id, target_user_name))
|
||||||
|
msg.append(f" 的抽卡记录\n\n")
|
||||||
|
else:
|
||||||
|
msg.append(format_user_mention(target_user_id, target_user_name) + "\n")
|
||||||
|
|
||||||
|
if not stats["success"]:
|
||||||
|
msg.append(f"该用户还没有抽卡记录哦!")
|
||||||
|
await query_matcher.finish(msg)
|
||||||
|
|
||||||
|
# 构建统计信息
|
||||||
|
msg.append(f"总抽卡次数:{stats['total_draws']}\n")
|
||||||
|
msg.append(f"R卡数量:{stats['R_count']}\n")
|
||||||
|
msg.append(f"SR卡数量:{stats['SR_count']}\n")
|
||||||
|
msg.append(f"SSR卡数量:{stats['SSR_count']}\n")
|
||||||
|
msg.append(f"SP卡数量:{stats['SP_count']}\n")
|
||||||
|
|
||||||
|
# 计算每种稀有度的比例
|
||||||
|
if stats['total_draws'] > 0:
|
||||||
|
msg.append("\n稀有度比例:\n")
|
||||||
|
msg.append(f"R: {stats['R_count']/stats['total_draws']*100:.2f}%\n")
|
||||||
|
msg.append(f"SR: {stats['SR_count']/stats['total_draws']*100:.2f}%\n")
|
||||||
|
msg.append(f"SSR: {stats['SSR_count']/stats['total_draws']*100:.2f}%\n")
|
||||||
|
msg.append(f"SP: {stats['SP_count']/stats['total_draws']*100:.2f}%\n")
|
||||||
|
|
||||||
|
# 添加最近抽卡记录
|
||||||
|
if stats["recent_draws"]:
|
||||||
|
msg.append("\n最近5次抽卡记录:\n")
|
||||||
|
for draw in reversed(stats["recent_draws"]):
|
||||||
|
msg.append(f"{draw['date']}: {draw['rarity']} - {draw['name']}\n")
|
||||||
|
|
||||||
|
await query_matcher.finish(msg)
|
||||||
|
|
||||||
|
# 自定义排行榜权限检查(仅检查白名单,不检查抽卡次数)
|
||||||
|
def check_rank_permission() -> Rule:
|
||||||
|
async def _checker(event: MessageEvent) -> bool:
|
||||||
|
# 允许特定用户在任何场景下使用
|
||||||
|
if event.user_id == ALLOWED_USER_ID:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 在允许的群聊中任何人都可以使用
|
||||||
|
if isinstance(event, GroupMessageEvent) and event.group_id == ALLOWED_GROUP_ID:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
return Rule(_checker)
|
||||||
|
|
||||||
|
|
||||||
|
rank_matcher = on_startswith(("抽卡排行","抽卡榜"), priority=1, rule=check_rank_permission())
|
||||||
|
|
||||||
|
@rank_matcher.handle()
|
||||||
|
async def handle_rank(bot: Bot, event: MessageEvent, state: T_State):
|
||||||
|
# 获取排行榜数据
|
||||||
|
rank_data = gacha_system.get_rank_list()
|
||||||
|
|
||||||
|
if not rank_data:
|
||||||
|
await rank_matcher.finish("暂无抽卡排行榜数据")
|
||||||
|
|
||||||
|
# 构建消息
|
||||||
|
msg = Message("✨┈┈┈┈┈ 抽卡排行榜 ┈┈┈┈┈✨\n")
|
||||||
|
msg.append("🏆 SSR/SP稀有度式神排行榜 🏆\n")
|
||||||
|
msg.append("┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n")
|
||||||
|
|
||||||
|
for i, (user_id, data) in enumerate(rank_data[:10], 1):
|
||||||
|
# 获取用户昵称
|
||||||
|
user_name = "未知用户"
|
||||||
|
try:
|
||||||
|
if isinstance(event, GroupMessageEvent):
|
||||||
|
# 群聊场景获取群名片或昵称
|
||||||
|
user_info = await bot.get_group_member_info(group_id=event.group_id, user_id=int(user_id))
|
||||||
|
user_name = user_info.get("card") or user_info.get("nickname", "未知用户")
|
||||||
|
else:
|
||||||
|
# 私聊场景获取昵称
|
||||||
|
user_info = await bot.get_stranger_info(user_id=int(user_id))
|
||||||
|
user_name = user_info.get("nickname", "未知用户")
|
||||||
|
except Exception as e:
|
||||||
|
# 如果获取失败,尝试从事件中获取发送者信息
|
||||||
|
if str(user_id) == str(event.user_id):
|
||||||
|
user_name = event.sender.card if isinstance(event, GroupMessageEvent) else event.sender.nickname
|
||||||
|
|
||||||
|
# 美化输出格式
|
||||||
|
rank_icon = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else f"{i}."
|
||||||
|
ssr_icon = "🌟"
|
||||||
|
sp_icon = "💫"
|
||||||
|
total = data['SSR_count'] + data['SP_count']
|
||||||
|
|
||||||
|
msg.append(f"{rank_icon} {user_name}\n")
|
||||||
|
msg.append(f" {ssr_icon}SSR: {data['SSR_count']}次 {sp_icon}SP: {data['SP_count']}次\n")
|
||||||
|
msg.append(f" 🔮总计: {total}次\n")
|
||||||
|
msg.append("┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n")
|
||||||
|
|
||||||
|
await rank_matcher.finish(msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@daily_stats_matcher.handle()
|
||||||
|
async def handle_daily_stats(bot: Bot, event: MessageEvent, state: T_State):
|
||||||
|
"""处理今日抽卡统计命令"""
|
||||||
|
result = gacha_system.get_daily_stats()
|
||||||
|
|
||||||
|
if not result["success"]:
|
||||||
|
await daily_stats_matcher.finish(f"❌ {result['message']}")
|
||||||
|
|
||||||
|
stats = result["stats"]
|
||||||
|
date = result["date"]
|
||||||
|
|
||||||
|
# 构建统计消息
|
||||||
|
msg = Message()
|
||||||
|
msg.append(f"📊 今日抽卡统计 ({date})\n\n")
|
||||||
|
msg.append(f"👥 参与人数:{stats['total_users']}人\n")
|
||||||
|
msg.append(f"🎲 总抽卡次数:{stats['total_draws']}次\n\n")
|
||||||
|
|
||||||
|
# 稀有度分布
|
||||||
|
msg.append("🎯 稀有度分布:\n")
|
||||||
|
if stats['total_draws'] > 0:
|
||||||
|
msg.append(f"📜 R:{stats['rarity_stats']['R']}张 ({stats['rarity_stats']['R']/stats['total_draws']*100:.1f}%)\n")
|
||||||
|
msg.append(f"⭐ SR:{stats['rarity_stats']['SR']}张 ({stats['rarity_stats']['SR']/stats['total_draws']*100:.1f}%)\n")
|
||||||
|
msg.append(f"🌟 SSR:{stats['rarity_stats']['SSR']}张 ({stats['rarity_stats']['SSR']/stats['total_draws']*100:.1f}%)\n")
|
||||||
|
msg.append(f"🌈 SP:{stats['rarity_stats']['SP']}张 ({stats['rarity_stats']['SP']/stats['total_draws']*100:.1f}%)\n\n")
|
||||||
|
else:
|
||||||
|
msg.append("暂无数据\n\n")
|
||||||
|
|
||||||
|
# SSR/SP排行榜
|
||||||
|
if stats['top_users']:
|
||||||
|
msg.append("🏆 今日SSR/SP排行榜:\n")
|
||||||
|
for i, user_data in enumerate(stats['top_users'][:5], 1):
|
||||||
|
user_id = user_data['user_id']
|
||||||
|
ssr_count = user_data['ssr_count']
|
||||||
|
|
||||||
|
# 尝试获取用户昵称
|
||||||
|
try:
|
||||||
|
user_info = await bot.get_stranger_info(user_id=int(user_id))
|
||||||
|
user_name = user_info.get('nickname', f'用户{user_id}')
|
||||||
|
except:
|
||||||
|
user_name = f'用户{user_id}'
|
||||||
|
|
||||||
|
if i == 1:
|
||||||
|
msg.append(f"🥇 {user_name}:{ssr_count}张\n")
|
||||||
|
elif i == 2:
|
||||||
|
msg.append(f"🥈 {user_name}:{ssr_count}张\n")
|
||||||
|
elif i == 3:
|
||||||
|
msg.append(f"🥉 {user_name}:{ssr_count}张\n")
|
||||||
|
else:
|
||||||
|
msg.append(f"🏅 {user_name}:{ssr_count}张\n")
|
||||||
|
else:
|
||||||
|
msg.append("🏆 今日还没有人抽到SSR/SP哦~")
|
||||||
|
|
||||||
|
await daily_stats_matcher.finish(msg)
|
||||||
|
|
||||||
|
# 抽卡介绍命令
|
||||||
|
intro_matcher = on_command("抽卡介绍", aliases=set(INTRO_COMMANDS), priority=5, rule=check_permission())
|
||||||
|
|
||||||
|
@intro_matcher.handle()
|
||||||
|
async def handle_intro(bot: Bot, event: MessageEvent, state: T_State):
|
||||||
|
"""处理抽卡介绍命令"""
|
||||||
|
|
||||||
|
# 构建介绍消息
|
||||||
|
msg = "🎮 阴阳师抽卡系统介绍 🎮\n\n"
|
||||||
|
|
||||||
|
# 抽卡机制
|
||||||
|
msg += "📋 抽卡机制:\n"
|
||||||
|
msg += f"• 每日限制:{DAILY_LIMIT}次免费抽卡\n"
|
||||||
|
msg += "• 稀有度概率:\n"
|
||||||
|
for rarity, prob in config.RARITY_PROBABILITY.items():
|
||||||
|
msg += f" - {rarity}: {prob}%\n"
|
||||||
|
msg += "\n"
|
||||||
|
|
||||||
|
# 可用指令
|
||||||
|
msg += "🎯 可用指令:\n"
|
||||||
|
msg += f"• 抽卡:{', '.join(GACHA_COMMANDS[:3])}\n"
|
||||||
|
msg += f"• 三连抽:{', '.join(TRIPLE_GACHA_COMMANDS)}\n"
|
||||||
|
msg += f"• 个人统计:{', '.join(STATS_COMMANDS[:2])}\n"
|
||||||
|
msg += f"• 今日统计:{', '.join(DAILY_STATS_COMMANDS[:2])}\n"
|
||||||
|
msg += f"• 查询成就:{', '.join(ACHIEVEMENT_COMMANDS)}\n"
|
||||||
|
msg += "• 查询抽卡/查询抽奖:查询自己的或@他人的抽卡数据\n"
|
||||||
|
msg += "• 抽卡排行/抽卡榜:查看SSR/SP排行榜\n"
|
||||||
|
msg += "\n"
|
||||||
|
|
||||||
|
# 成就系统
|
||||||
|
msg += "🏆 成就系统:\n"
|
||||||
|
msg += "\n📅 勤勤恳恳系列(连续抽卡):\n"
|
||||||
|
consecutive_achievements = [
|
||||||
|
("勤勤恳恳Ⅰ", "30天", "天卡"),
|
||||||
|
("勤勤恳恳Ⅱ", "60天", "天卡"),
|
||||||
|
("勤勤恳恳Ⅲ", "90天", "天卡"),
|
||||||
|
("勤勤恳恳Ⅳ", "120天", "周卡"),
|
||||||
|
("勤勤恳恳Ⅴ", "150天", "周卡")
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, days, reward in consecutive_achievements:
|
||||||
|
msg += f"• {name}:连续{days} → {reward} 💎\n"
|
||||||
|
msg += " ※ 达到最高等级后每30天可重复获得天卡奖励\n\n"
|
||||||
|
|
||||||
|
msg += "😭 非酋系列(无SSR/SP连击):\n"
|
||||||
|
no_ssr_achievements = [
|
||||||
|
("非酋", "60次", "天卡"),
|
||||||
|
("顶级非酋", "120次", "周卡"),
|
||||||
|
("月见黑", "180次", "月卡")
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, count, reward in no_ssr_achievements:
|
||||||
|
msg += f"• {name}:连续{count}未中SSR/SP → {reward} 💎\n"
|
||||||
|
msg += "\n"
|
||||||
|
|
||||||
|
# 奖励说明
|
||||||
|
msg += "🎁 奖励说明:\n"
|
||||||
|
msg += "• 天卡:蛋定助手天卡奖励\n"
|
||||||
|
msg += "• 周卡:蛋定助手周卡奖励\n"
|
||||||
|
msg += "• 月卡:蛋定助手月卡奖励\n"
|
||||||
|
msg += "\n"
|
||||||
|
|
||||||
|
# 联系管理员
|
||||||
|
msg += "📞 重要提醒:\n"
|
||||||
|
msg += "🔸 所有奖励需要联系管理员获取 🔸\n"
|
||||||
|
msg += "请在获得成就后主动联系管理员领取奖励哦~ 😊\n\n"
|
||||||
|
|
||||||
|
# 祝福语
|
||||||
|
msg += "🍀 祝您抽卡愉快,欧气满满! ✨"
|
||||||
|
|
||||||
|
await intro_matcher.finish(msg)
|
||||||
|
|
||||||
|
# 导入 Web API 模块,使其路由能够注册到 NoneBot 的 FastAPI 应用
|
||||||
|
from . import web_api
|
||||||
|
|
||||||
|
# 注册 Web 路由
|
||||||
|
try:
|
||||||
|
web_api.register_web_routes()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 注册 onmyoji_gacha Web 路由失败: {e}")
|
||||||
216
danding_bot/plugins/onmyoji_gacha/api_utils.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
from nonebot import logger
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
def mask_username(username: str) -> str:
|
||||||
|
"""
|
||||||
|
对用户名进行脱敏处理,只显示前两位和后两位,中间用*号隐藏
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: 原始用户名
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
脱敏后的用户名
|
||||||
|
"""
|
||||||
|
if not username:
|
||||||
|
return username
|
||||||
|
|
||||||
|
# 如果用户名长度小于等于4,直接显示前两位和后两位(可能重叠)
|
||||||
|
if len(username) <= 4:
|
||||||
|
return username
|
||||||
|
|
||||||
|
# 显示前两位和后两位,中间用*号填充
|
||||||
|
return f"{username[:2]}{'*' * (len(username) - 4)}{username[-2:]}"
|
||||||
|
|
||||||
|
# 获取配置
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
# API 端点配置
|
||||||
|
DD_API_HOST = "https://api.danding.vip/DD/" # 蛋定服务器连接地址
|
||||||
|
BOT_TOKEN = "3340e353a49447f1be640543cbdcd937" # 对接服务器的Token
|
||||||
|
BOT_USER_ID = "1424473282" # 机器人用户ID
|
||||||
|
|
||||||
|
async def query_qq_binding(qq: str) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||||
|
"""
|
||||||
|
查询QQ号是否绑定了蛋定用户名
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qq: 要查询的QQ号
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[是否绑定, 用户名, VIP到期时间]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{DD_API_HOST}query_qq_binding"
|
||||||
|
data = {"qq": qq}
|
||||||
|
|
||||||
|
response = requests.post(url=url, json=data)
|
||||||
|
logger.debug(f"查询QQ绑定状态响应: {response}")
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(f"查询QQ绑定状态失败,状态码: {response.status_code}")
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
logger.debug(f"查询QQ绑定状态结果: {result}")
|
||||||
|
|
||||||
|
if result.get("code") == 200:
|
||||||
|
data = result.get("data", {})
|
||||||
|
is_bound = data.get("is_bound", False)
|
||||||
|
|
||||||
|
if is_bound:
|
||||||
|
username = data.get("username")
|
||||||
|
vip_time = data.get("vip_time")
|
||||||
|
return True, username, vip_time
|
||||||
|
else:
|
||||||
|
return False, None, None
|
||||||
|
else:
|
||||||
|
logger.error(f"查询QQ绑定状态失败,错误信息: {result.get('message')}")
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询QQ绑定状态异常: {str(e)}")
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
async def add_user_viptime(username: str, time_class: str = "Day") -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
为用户添加VIP时间
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: 蛋定用户名
|
||||||
|
time_class: 时间类型 (Hour/Day/Week/Month/Season/Year)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[是否成功, 响应消息]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{DD_API_HOST}bot_add_user_viptime"
|
||||||
|
data = {
|
||||||
|
"user": BOT_USER_ID,
|
||||||
|
"token": BOT_TOKEN,
|
||||||
|
"username": username,
|
||||||
|
"classes": time_class
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url=url, json=data)
|
||||||
|
logger.debug(f"添加VIP时间响应: {response}")
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
error_msg = f"添加VIP时间失败,状态码: {response.status_code}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
logger.debug(f"添加VIP时间结果: {result}")
|
||||||
|
|
||||||
|
if result.get("code") == 200:
|
||||||
|
return True, result.get("msg", "添加VIP时间成功")
|
||||||
|
else:
|
||||||
|
error_msg = result.get("msg", "添加VIP时间失败")
|
||||||
|
logger.error(f"添加VIP时间失败: {error_msg}")
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"添加VIP时间异常: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return False, error_msg
|
||||||
|
|
||||||
|
async def process_ssr_sp_reward(user_id: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
处理SSR/SP奖励发放
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: QQ用户ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[是否自动发放成功, 消息内容]
|
||||||
|
"""
|
||||||
|
# 查询QQ绑定状态
|
||||||
|
is_bound, username, vip_time = await query_qq_binding(user_id)
|
||||||
|
|
||||||
|
if not is_bound:
|
||||||
|
# 用户未绑定,返回提示信息
|
||||||
|
msg = (f"🎉恭喜您抽中了SSR/SP稀有度式神!🎉\n"
|
||||||
|
f"获得奖励:蛋定助手天卡一张\n"
|
||||||
|
f"获取奖励请联系管理员,或前往蛋定云服务中绑定QQ号即可体验自动加时!")
|
||||||
|
return False, msg
|
||||||
|
else:
|
||||||
|
# 用户已绑定,自动加时
|
||||||
|
success, message = await add_user_viptime(username, "Day")
|
||||||
|
|
||||||
|
if success:
|
||||||
|
masked_username = mask_username(username)
|
||||||
|
msg = (f"🎉恭喜您抽中了SSR/SP稀有度式神!🎉\n"
|
||||||
|
f"🎁已自动为您的蛋定账号({masked_username})添加天卡时长!\n"
|
||||||
|
f"📅到期时间: {message.split('到期时间为:')[-1] if '到期时间为:' in message else '请查看您的账号详情'}")
|
||||||
|
return True, msg
|
||||||
|
else:
|
||||||
|
# 自动加时失败,返回错误信息和手动领取提示
|
||||||
|
msg = (f"🎉恭喜您抽中了SSR/SP稀有度式神!🎉\n"
|
||||||
|
f"获得奖励:蛋定助手天卡一张\n"
|
||||||
|
f"⚠️自动加时失败: {message}\n"
|
||||||
|
f"请联系管理员手动领取奖励!")
|
||||||
|
return False, msg
|
||||||
|
|
||||||
|
async def process_achievement_reward(user_id: str, achievement_id: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
处理成就奖励发放
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: QQ用户ID
|
||||||
|
achievement_id: 成就ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[是否自动发放成功, 消息内容]
|
||||||
|
"""
|
||||||
|
# 获取成就配置
|
||||||
|
achievement_config = config.ACHIEVEMENTS.get(achievement_id)
|
||||||
|
if not achievement_config:
|
||||||
|
# 检查是否是重复奖励
|
||||||
|
if "_repeat_" in achievement_id:
|
||||||
|
base_achievement_id = achievement_id.split("_repeat_")[0]
|
||||||
|
base_config = config.ACHIEVEMENTS.get(base_achievement_id)
|
||||||
|
if base_config:
|
||||||
|
reward_type = base_config.get("repeat_reward", "天卡")
|
||||||
|
else:
|
||||||
|
reward_type = "天卡"
|
||||||
|
else:
|
||||||
|
return False, f"未找到成就配置: {achievement_id}"
|
||||||
|
else:
|
||||||
|
reward_type = achievement_config.get("reward", "天卡")
|
||||||
|
|
||||||
|
# 查询QQ绑定状态
|
||||||
|
is_bound, username, vip_time = await query_qq_binding(user_id)
|
||||||
|
|
||||||
|
if not is_bound:
|
||||||
|
# 用户未绑定,返回提示信息
|
||||||
|
msg = (f"🏆 恭喜解锁成就奖励!\n"
|
||||||
|
f"获得奖励:蛋定助手{reward_type}一张\n"
|
||||||
|
f"获取奖励请联系管理员,或前往蛋定云服务中绑定QQ号即可体验自动加时!")
|
||||||
|
return False, msg
|
||||||
|
else:
|
||||||
|
# 用户已绑定,自动加时
|
||||||
|
# 将奖励类型转换为API需要的时间类型
|
||||||
|
time_class = "Day" # 默认为天卡
|
||||||
|
if "周卡" in reward_type:
|
||||||
|
time_class = "Week"
|
||||||
|
elif "月卡" in reward_type:
|
||||||
|
time_class = "Month"
|
||||||
|
|
||||||
|
success, message = await add_user_viptime(username, time_class)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
masked_username = mask_username(username)
|
||||||
|
msg = (f"🏆 恭喜解锁成就奖励!\n"
|
||||||
|
f"🎁已自动为您的蛋定账号({masked_username})添加{reward_type}时长!\n"
|
||||||
|
f"📅到期时间: {message.split('到期时间为:')[-1] if '到期时间为:' in message else '请查看您的账号详情'}")
|
||||||
|
return True, msg
|
||||||
|
else:
|
||||||
|
# 自动加时失败,返回错误信息和手动领取提示
|
||||||
|
msg = (f"🏆 恭喜解锁成就奖励!\n"
|
||||||
|
f"获得奖励:蛋定助手{reward_type}一张\n"
|
||||||
|
f"⚠️自动加时失败: {message}\n"
|
||||||
|
f"请联系管理员手动领取奖励!")
|
||||||
|
return False, msg
|
||||||
118
danding_bot/plugins/onmyoji_gacha/config.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
from pydantic import BaseSettings
|
||||||
|
import os
|
||||||
|
|
||||||
|
class Config(BaseSettings):
|
||||||
|
# 抽卡概率配置
|
||||||
|
RARITY_PROBABILITY: dict = {
|
||||||
|
"R": 78.75,
|
||||||
|
"SR": 20.0,
|
||||||
|
"SSR": 1.0,
|
||||||
|
"SP": 0.25
|
||||||
|
}
|
||||||
|
|
||||||
|
# 每日抽卡限制
|
||||||
|
DAILY_LIMIT: int = 3
|
||||||
|
|
||||||
|
# 数据文件路径
|
||||||
|
DB_FILE: str = "data/onmyoji_gacha/gacha.db"
|
||||||
|
DAILY_DRAWS_FILE: str = "data/onmyoji_gacha/daily_draws.json" # 保留用于迁移
|
||||||
|
USER_STATS_FILE: str = "data/onmyoji_gacha/user_stats.json" # 保留用于迁移
|
||||||
|
|
||||||
|
# 式神图片目录
|
||||||
|
SHIKIGAMI_IMG_DIR: str = "data/chouka/"
|
||||||
|
|
||||||
|
# 触发指令
|
||||||
|
GACHA_COMMANDS: list = ["抽卡","抽奖", "召唤"]
|
||||||
|
STATS_COMMANDS: list = ["我的抽卡","我的抽奖", "我的图鉴"]
|
||||||
|
DAILY_STATS_COMMANDS: list = ["今日抽卡", "今日统计", "抽卡统计"]
|
||||||
|
TRIPLE_GACHA_COMMANDS: list = ["三连", "三连抽"]
|
||||||
|
ACHIEVEMENT_COMMANDS: list = ["查询成就", "抽卡成就"]
|
||||||
|
INTRO_COMMANDS: list = ["抽卡介绍", "抽卡说明", "抽卡帮助"]
|
||||||
|
|
||||||
|
# 成就系统配置
|
||||||
|
ACHIEVEMENTS: dict = {
|
||||||
|
"consecutive_days_30_1": {
|
||||||
|
"name": "勤勤恳恳Ⅰ",
|
||||||
|
"description": "连续抽卡30天",
|
||||||
|
"reward": "天卡",
|
||||||
|
"threshold": 30,
|
||||||
|
"type": "consecutive_days",
|
||||||
|
"level": 1,
|
||||||
|
"repeatable": True
|
||||||
|
},
|
||||||
|
"consecutive_days_30_2": {
|
||||||
|
"name": "勤勤恳恳Ⅱ",
|
||||||
|
"description": "连续抽卡60天",
|
||||||
|
"reward": "天卡",
|
||||||
|
"threshold": 60,
|
||||||
|
"type": "consecutive_days",
|
||||||
|
"level": 2,
|
||||||
|
"repeatable": True
|
||||||
|
},
|
||||||
|
"consecutive_days_30_3": {
|
||||||
|
"name": "勤勤恳恳Ⅲ",
|
||||||
|
"description": "连续抽卡90天",
|
||||||
|
"reward": "天卡",
|
||||||
|
"threshold": 90,
|
||||||
|
"type": "consecutive_days",
|
||||||
|
"level": 3,
|
||||||
|
"repeatable": True
|
||||||
|
},
|
||||||
|
"consecutive_days_30_4": {
|
||||||
|
"name": "勤勤恳恳Ⅳ",
|
||||||
|
"description": "连续抽卡120天",
|
||||||
|
"reward": "周卡",
|
||||||
|
"threshold": 120,
|
||||||
|
"type": "consecutive_days",
|
||||||
|
"level": 4,
|
||||||
|
"repeatable": True
|
||||||
|
},
|
||||||
|
"consecutive_days_30_5": {
|
||||||
|
"name": "勤勤恳恳Ⅴ",
|
||||||
|
"description": "连续抽卡150天",
|
||||||
|
"reward": "周卡",
|
||||||
|
"threshold": 150,
|
||||||
|
"type": "consecutive_days",
|
||||||
|
"level": 5,
|
||||||
|
"repeatable": True,
|
||||||
|
"repeat_reward": "天卡"
|
||||||
|
},
|
||||||
|
"no_ssr_60": {
|
||||||
|
"name": "非酋",
|
||||||
|
"description": "连续60次未抽到SSR/SP",
|
||||||
|
"reward": "天卡",
|
||||||
|
"threshold": 60,
|
||||||
|
"type": "no_ssr_streak"
|
||||||
|
},
|
||||||
|
"no_ssr_120": {
|
||||||
|
"name": "顶级非酋",
|
||||||
|
"description": "连续120次未抽到SSR/SP",
|
||||||
|
"reward": "周卡",
|
||||||
|
"threshold": 120,
|
||||||
|
"type": "no_ssr_streak"
|
||||||
|
},
|
||||||
|
"no_ssr_180": {
|
||||||
|
"name": "月见黑",
|
||||||
|
"description": "连续180次未抽到SSR/SP",
|
||||||
|
"reward": "月卡",
|
||||||
|
"threshold": 180,
|
||||||
|
"type": "no_ssr_streak"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 权限配置
|
||||||
|
ALLOWED_GROUP_ID: int = 621016172
|
||||||
|
ALLOWED_USER_ID: int = 1424473282
|
||||||
|
|
||||||
|
# 特殊概率用户配置
|
||||||
|
SPECIAL_PROBABILITY_USERS: list = ["1424473282"] # 100%抽到SSR或SP的用户列表
|
||||||
|
|
||||||
|
# Web后台管理配置
|
||||||
|
WEB_ADMIN_TOKEN: str = os.getenv("WEB_ADMIN_TOKEN", "onmyoji_admin_token_2024")
|
||||||
|
WEB_ADMIN_PORT: int = int(os.getenv("WEB_ADMIN_PORT", "8080"))
|
||||||
|
|
||||||
|
# 时区
|
||||||
|
TIMEZONE: str = "Asia/Shanghai"
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
extra = "ignore"
|
||||||
BIN
danding_bot/plugins/onmyoji_gacha/data/onmyoji_gacha/gacha.db
Normal file
616
danding_bot/plugins/onmyoji_gacha/data_manager.py
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import datetime
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
# 创建Config实例
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
class DataManager:
|
||||||
|
def __init__(self):
|
||||||
|
# 确保目录存在
|
||||||
|
os.makedirs(os.path.dirname(config.DB_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
# 初始化数据库
|
||||||
|
self._init_db()
|
||||||
|
|
||||||
|
# 加载式神数据
|
||||||
|
self.shikigami_data = self._load_shikigami_data()
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
"""初始化数据库"""
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 创建式神表
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS shikigami (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
rarity TEXT NOT NULL,
|
||||||
|
image_path TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 创建每日抽卡记录表
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS daily_draws (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
rarity TEXT NOT NULL,
|
||||||
|
shikigami_id INTEGER NOT NULL,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (shikigami_id) REFERENCES shikigami(id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 创建用户统计表
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS user_stats (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
total_draws INTEGER DEFAULT 0,
|
||||||
|
R_count INTEGER DEFAULT 0,
|
||||||
|
SR_count INTEGER DEFAULT 0,
|
||||||
|
SSR_count INTEGER DEFAULT 0,
|
||||||
|
SP_count INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 创建抽卡历史表
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS draw_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
rarity TEXT NOT NULL,
|
||||||
|
shikigami_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES user_stats(user_id),
|
||||||
|
FOREIGN KEY (shikigami_id) REFERENCES shikigami(id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 创建成就表
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS achievements (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
achievement_id TEXT NOT NULL,
|
||||||
|
unlocked_date TEXT NOT NULL,
|
||||||
|
reward_claimed INTEGER DEFAULT 0,
|
||||||
|
UNIQUE(user_id, achievement_id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 创建用户成就进度表
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS user_achievement_progress (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
consecutive_days INTEGER DEFAULT 0,
|
||||||
|
last_draw_date TEXT DEFAULT '',
|
||||||
|
no_ssr_streak INTEGER DEFAULT 0,
|
||||||
|
total_consecutive_days INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def update_achievement_progress(self, user_id: str, rarity: str) -> List[str]:
|
||||||
|
"""更新用户成就进度,返回新解锁的成就列表"""
|
||||||
|
today = self.get_today_date()
|
||||||
|
unlocked_achievements = []
|
||||||
|
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 获取或创建用户成就进度
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT * FROM user_achievement_progress WHERE user_id = ?",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
progress = cursor.fetchone()
|
||||||
|
|
||||||
|
if not progress:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO user_achievement_progress (user_id, last_draw_date) VALUES (?, ?)",
|
||||||
|
(user_id, today)
|
||||||
|
)
|
||||||
|
consecutive_days = 1
|
||||||
|
no_ssr_streak = 1 if rarity not in ["SSR", "SP"] else 0
|
||||||
|
total_consecutive_days = 1
|
||||||
|
else:
|
||||||
|
last_draw_date = progress[2]
|
||||||
|
consecutive_days = progress[1]
|
||||||
|
no_ssr_streak = progress[3]
|
||||||
|
total_consecutive_days = progress[4]
|
||||||
|
|
||||||
|
# 更新连续抽卡天数
|
||||||
|
if last_draw_date != today:
|
||||||
|
# 检查是否是连续的一天
|
||||||
|
last_date = datetime.datetime.strptime(last_draw_date, "%Y-%m-%d")
|
||||||
|
current_date = datetime.datetime.strptime(today, "%Y-%m-%d")
|
||||||
|
days_diff = (current_date - last_date).days
|
||||||
|
|
||||||
|
if days_diff == 1:
|
||||||
|
consecutive_days += 1
|
||||||
|
total_consecutive_days += 1
|
||||||
|
elif days_diff > 1:
|
||||||
|
consecutive_days = 1
|
||||||
|
total_consecutive_days += 1
|
||||||
|
# days_diff == 0 表示今天已经抽过卡了,不更新连续天数
|
||||||
|
|
||||||
|
# 更新无SSR连击数
|
||||||
|
if rarity in ["SSR", "SP"]:
|
||||||
|
no_ssr_streak = 0
|
||||||
|
else:
|
||||||
|
no_ssr_streak += 1
|
||||||
|
|
||||||
|
# 更新进度
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT OR REPLACE INTO user_achievement_progress
|
||||||
|
(user_id, consecutive_days, last_draw_date, no_ssr_streak, total_consecutive_days)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""", (user_id, consecutive_days, today, no_ssr_streak, total_consecutive_days))
|
||||||
|
|
||||||
|
# 检查是否解锁新成就
|
||||||
|
for achievement_id, achievement_config in config.ACHIEVEMENTS.items():
|
||||||
|
# 对于可重复获得的成就(勤勤恳恳系列),需要特殊处理
|
||||||
|
if achievement_config.get("repeatable", False) and achievement_config["type"] == "consecutive_days":
|
||||||
|
# 检查连续抽卡成就的升级逻辑
|
||||||
|
if consecutive_days >= achievement_config["threshold"]:
|
||||||
|
# 检查是否已经解锁过这个等级
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM achievements WHERE user_id = ? AND achievement_id = ?",
|
||||||
|
(user_id, achievement_id)
|
||||||
|
)
|
||||||
|
if not cursor.fetchone():
|
||||||
|
# 解锁新等级的成就
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO achievements (user_id, achievement_id, unlocked_date)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""", (user_id, achievement_id, today))
|
||||||
|
unlocked_achievements.append(achievement_id)
|
||||||
|
|
||||||
|
# 如果是最高等级(Ⅴ),检查是否需要给重复奖励
|
||||||
|
elif achievement_config["level"] == 5 and consecutive_days >= 150:
|
||||||
|
# 每30天给一次重复奖励
|
||||||
|
days_over_150 = consecutive_days - 150
|
||||||
|
if days_over_150 > 0 and days_over_150 % 30 == 0:
|
||||||
|
# 检查这个重复奖励是否已经给过
|
||||||
|
repeat_id = f"{achievement_id}_repeat_{days_over_150//30}"
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM achievements WHERE user_id = ? AND achievement_id = ?",
|
||||||
|
(user_id, repeat_id)
|
||||||
|
)
|
||||||
|
if not cursor.fetchone():
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO achievements (user_id, achievement_id, unlocked_date)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""", (user_id, repeat_id, today))
|
||||||
|
unlocked_achievements.append(achievement_id)
|
||||||
|
else:
|
||||||
|
# 非重复成就的原有逻辑
|
||||||
|
# 检查是否已经解锁
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM achievements WHERE user_id = ? AND achievement_id = ?",
|
||||||
|
(user_id, achievement_id)
|
||||||
|
)
|
||||||
|
if cursor.fetchone():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查成就条件
|
||||||
|
unlocked = False
|
||||||
|
if achievement_config["type"] == "consecutive_days":
|
||||||
|
if consecutive_days >= achievement_config["threshold"]:
|
||||||
|
unlocked = True
|
||||||
|
elif achievement_config["type"] == "no_ssr_streak":
|
||||||
|
if no_ssr_streak >= achievement_config["threshold"]:
|
||||||
|
unlocked = True
|
||||||
|
|
||||||
|
if unlocked:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO achievements (user_id, achievement_id, unlocked_date)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""", (user_id, achievement_id, today))
|
||||||
|
unlocked_achievements.append(achievement_id)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return unlocked_achievements
|
||||||
|
|
||||||
|
def get_user_achievements(self, user_id: str) -> Dict[str, Any]:
|
||||||
|
"""获取用户成就信息"""
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 获取已解锁的成就
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT achievement_id, unlocked_date, reward_claimed FROM achievements WHERE user_id = ?",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
unlocked = {row["achievement_id"]: {
|
||||||
|
"unlocked_date": row["unlocked_date"],
|
||||||
|
"reward_claimed": bool(row["reward_claimed"])
|
||||||
|
} for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
# 获取进度
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT * FROM user_achievement_progress WHERE user_id = ?",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
progress_row = cursor.fetchone()
|
||||||
|
|
||||||
|
if not progress_row:
|
||||||
|
progress = {
|
||||||
|
"consecutive_days": 0,
|
||||||
|
"no_ssr_streak": 0,
|
||||||
|
"total_consecutive_days": 0
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
progress = {
|
||||||
|
"consecutive_days": progress_row["consecutive_days"],
|
||||||
|
"no_ssr_streak": progress_row["no_ssr_streak"],
|
||||||
|
"total_consecutive_days": progress_row["total_consecutive_days"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"unlocked": unlocked,
|
||||||
|
"progress": progress
|
||||||
|
}
|
||||||
|
|
||||||
|
def claim_achievement_reward(self, user_id: str, achievement_id: str) -> bool:
|
||||||
|
"""领取成就奖励"""
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE achievements
|
||||||
|
SET reward_claimed = 1
|
||||||
|
WHERE user_id = ? AND achievement_id = ? AND reward_claimed = 0
|
||||||
|
""", (user_id, achievement_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
# 迁移现有JSON数据到SQLite
|
||||||
|
self._migrate_data()
|
||||||
|
|
||||||
|
def _migrate_data(self):
|
||||||
|
"""迁移JSON数据到SQLite"""
|
||||||
|
try:
|
||||||
|
# 迁移每日抽卡记录
|
||||||
|
if os.path.exists(config.DAILY_DRAWS_FILE):
|
||||||
|
with open(config.DAILY_DRAWS_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
daily_draws = json.load(f)
|
||||||
|
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
for date, users in daily_draws.items():
|
||||||
|
for user_id, draws in users.items():
|
||||||
|
for draw in draws:
|
||||||
|
# 查找式神ID
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM shikigami WHERE name=? AND rarity=?",
|
||||||
|
(draw["name"], draw["rarity"])
|
||||||
|
)
|
||||||
|
shikigami_id = cursor.fetchone()
|
||||||
|
|
||||||
|
if shikigami_id:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO daily_draws (date, user_id, rarity, shikigami_id, timestamp) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
(date, user_id, draw["rarity"], shikigami_id[0], draw["timestamp"])
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 迁移用户统计数据
|
||||||
|
if os.path.exists(config.USER_STATS_FILE):
|
||||||
|
with open(config.USER_STATS_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
user_stats = json.load(f)
|
||||||
|
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
for user_id, stats in user_stats.items():
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR REPLACE INTO user_stats (user_id, total_draws, R_count, SR_count, SSR_count, SP_count) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
(user_id, stats["total_draws"], stats["R_count"], stats["SR_count"], stats["SSR_count"], stats["SP_count"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# 迁移抽卡历史
|
||||||
|
for draw in stats.get("draw_history", []):
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM shikigami WHERE name=? AND rarity=?",
|
||||||
|
(draw["name"], draw["rarity"])
|
||||||
|
)
|
||||||
|
shikigami_id = cursor.fetchone()
|
||||||
|
|
||||||
|
if shikigami_id:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO draw_history (user_id, date, rarity, shikigami_id) VALUES (?, ?, ?, ?)",
|
||||||
|
(user_id, draw["date"], draw["rarity"], shikigami_id[0])
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"数据迁移失败: {e}")
|
||||||
|
|
||||||
|
def _load_shikigami_data(self) -> Dict[str, List[Dict[str, str]]]:
|
||||||
|
"""加载式神数据到数据库"""
|
||||||
|
result = {"R": [], "SR": [], "SSR": [], "SP": []}
|
||||||
|
rarity_dirs = {
|
||||||
|
"R": "r",
|
||||||
|
"SR": "sr",
|
||||||
|
"SSR": "ssr",
|
||||||
|
"SP": "sp"
|
||||||
|
}
|
||||||
|
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 清空现有式神数据
|
||||||
|
cursor.execute("DELETE FROM shikigami")
|
||||||
|
|
||||||
|
for rarity, dir_name in rarity_dirs.items():
|
||||||
|
dir_path = os.path.join(config.SHIKIGAMI_IMG_DIR, dir_name)
|
||||||
|
if os.path.exists(dir_path):
|
||||||
|
for file_name in os.listdir(dir_path):
|
||||||
|
if file_name.endswith(('.png', '.jpg', '.jpeg')):
|
||||||
|
name = os.path.splitext(file_name)[0]
|
||||||
|
image_path = os.path.join(dir_path, file_name)
|
||||||
|
|
||||||
|
# 插入式神数据
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO shikigami (name, rarity, image_path) VALUES (?, ?, ?)",
|
||||||
|
(name, rarity, image_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
result[rarity].append({
|
||||||
|
"name": name,
|
||||||
|
"image_url": image_path
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_today_date(self) -> str:
|
||||||
|
"""获取当前日期字符串"""
|
||||||
|
return datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
def get_current_time(self) -> str:
|
||||||
|
"""获取当前时间字符串"""
|
||||||
|
return datetime.datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
def get_daily_draws(self) -> Dict[str, Dict[str, List[Dict[str, str]]]]:
|
||||||
|
"""获取每日抽卡记录"""
|
||||||
|
result = {}
|
||||||
|
today = self.get_today_date()
|
||||||
|
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 先查询今日的抽卡记录
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT date, user_id, rarity, shikigami_id, timestamp
|
||||||
|
FROM daily_draws
|
||||||
|
WHERE date = ?
|
||||||
|
ORDER BY timestamp
|
||||||
|
""", (today,))
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
# 获取所有涉及的式神ID
|
||||||
|
shikigami_ids = list(set(row["shikigami_id"] for row in rows))
|
||||||
|
|
||||||
|
# 查询式神信息
|
||||||
|
shikigami_info = {}
|
||||||
|
if shikigami_ids:
|
||||||
|
placeholders = ','.join('?' * len(shikigami_ids))
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT id, name, rarity
|
||||||
|
FROM shikigami
|
||||||
|
WHERE id IN ({placeholders})
|
||||||
|
""", shikigami_ids)
|
||||||
|
|
||||||
|
for shikigami_row in cursor.fetchall():
|
||||||
|
shikigami_info[shikigami_row["id"]] = {
|
||||||
|
"name": shikigami_row["name"],
|
||||||
|
"rarity": shikigami_row["rarity"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建结果
|
||||||
|
for row in rows:
|
||||||
|
date = row["date"]
|
||||||
|
user_id = row["user_id"]
|
||||||
|
shikigami_id = row["shikigami_id"]
|
||||||
|
|
||||||
|
if date not in result:
|
||||||
|
result[date] = {}
|
||||||
|
|
||||||
|
if user_id not in result[date]:
|
||||||
|
result[date][user_id] = []
|
||||||
|
|
||||||
|
# 如果找不到式神信息,使用daily_draws表中的稀有度和默认名称
|
||||||
|
if shikigami_id in shikigami_info:
|
||||||
|
name = shikigami_info[shikigami_id]["name"]
|
||||||
|
rarity = shikigami_info[shikigami_id]["rarity"]
|
||||||
|
else:
|
||||||
|
name = f"式神{shikigami_id}"
|
||||||
|
rarity = row["rarity"]
|
||||||
|
|
||||||
|
result[date][user_id].append({
|
||||||
|
"rarity": rarity,
|
||||||
|
"name": name,
|
||||||
|
"timestamp": row["timestamp"]
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def save_daily_draws(self, data: Dict[str, Dict[str, List[Dict[str, str]]]]):
|
||||||
|
"""保存每日抽卡记录"""
|
||||||
|
# SQLite实现中此方法为空,因为记录时直接插入数据库
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_user_stats(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""获取用户统计数据"""
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 获取基础统计
|
||||||
|
cursor.execute("SELECT * FROM user_stats")
|
||||||
|
user_stats = cursor.fetchall()
|
||||||
|
|
||||||
|
for stat in user_stats:
|
||||||
|
user_id = stat["user_id"]
|
||||||
|
result[user_id] = {
|
||||||
|
"total_draws": stat["total_draws"],
|
||||||
|
"R_count": stat["R_count"],
|
||||||
|
"SR_count": stat["SR_count"],
|
||||||
|
"SSR_count": stat["SSR_count"],
|
||||||
|
"SP_count": stat["SP_count"],
|
||||||
|
"draw_history": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取抽卡历史
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT draw_history.date, draw_history.rarity, shikigami.name
|
||||||
|
FROM draw_history
|
||||||
|
JOIN shikigami ON draw_history.shikigami_id = shikigami.id
|
||||||
|
WHERE draw_history.user_id = ?
|
||||||
|
ORDER BY draw_history.date DESC
|
||||||
|
LIMIT 100
|
||||||
|
""", (user_id,))
|
||||||
|
|
||||||
|
history = cursor.fetchall()
|
||||||
|
result[user_id]["draw_history"] = [
|
||||||
|
{
|
||||||
|
"date": row["date"],
|
||||||
|
"rarity": row["rarity"],
|
||||||
|
"name": row["name"]
|
||||||
|
} for row in history
|
||||||
|
]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def save_user_stats(self, data: Dict[str, Dict[str, Any]]):
|
||||||
|
"""保存用户统计数据"""
|
||||||
|
# SQLite实现中此方法为空,因为统计时直接更新数据库
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_daily_limit(self, user_id: str) -> bool:
|
||||||
|
"""检查用户是否达到每日抽卡限制"""
|
||||||
|
today = self.get_today_date()
|
||||||
|
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM daily_draws
|
||||||
|
WHERE date = ? AND user_id = ?
|
||||||
|
""", (today, user_id))
|
||||||
|
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
return count < config.DAILY_LIMIT
|
||||||
|
|
||||||
|
def get_draws_left(self, user_id: str) -> int:
|
||||||
|
"""获取用户今日剩余抽卡次数"""
|
||||||
|
today = self.get_today_date()
|
||||||
|
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM daily_draws
|
||||||
|
WHERE date = ? AND user_id = ?
|
||||||
|
""", (today, user_id))
|
||||||
|
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
return max(0, config.DAILY_LIMIT - count)
|
||||||
|
|
||||||
|
def record_draw(self, user_id: str, rarity: str, shikigami_name: str) -> List[str]:
|
||||||
|
"""记录一次抽卡,返回新解锁的成就列表"""
|
||||||
|
today = self.get_today_date()
|
||||||
|
current_time = self.get_current_time()
|
||||||
|
|
||||||
|
with sqlite3.connect(config.DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 获取式神ID
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM shikigami WHERE name = ? AND rarity = ?",
|
||||||
|
(shikigami_name, rarity)
|
||||||
|
)
|
||||||
|
shikigami_id = cursor.fetchone()
|
||||||
|
|
||||||
|
if not shikigami_id:
|
||||||
|
logging.error(f"找不到式神: {shikigami_name} ({rarity})")
|
||||||
|
return []
|
||||||
|
|
||||||
|
shikigami_id = shikigami_id[0]
|
||||||
|
|
||||||
|
# 记录每日抽卡
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO daily_draws (date, user_id, rarity, shikigami_id, timestamp)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""", (today, user_id, rarity, shikigami_id, current_time))
|
||||||
|
|
||||||
|
# 更新用户统计
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT OR IGNORE INTO user_stats (user_id) VALUES (?)
|
||||||
|
""", (user_id,))
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE user_stats
|
||||||
|
SET total_draws = total_draws + 1,
|
||||||
|
R_count = R_count + ?,
|
||||||
|
SR_count = SR_count + ?,
|
||||||
|
SSR_count = SSR_count + ?,
|
||||||
|
SP_count = SP_count + ?
|
||||||
|
WHERE user_id = ?
|
||||||
|
""", (
|
||||||
|
1 if rarity == "R" else 0,
|
||||||
|
1 if rarity == "SR" else 0,
|
||||||
|
1 if rarity == "SSR" else 0,
|
||||||
|
1 if rarity == "SP" else 0,
|
||||||
|
user_id
|
||||||
|
))
|
||||||
|
|
||||||
|
# 添加抽卡历史
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO draw_history (user_id, date, rarity, shikigami_id)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""", (user_id, today, rarity, shikigami_id))
|
||||||
|
|
||||||
|
# 保持历史记录不超过100条
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE FROM draw_history
|
||||||
|
WHERE user_id = ? AND id NOT IN (
|
||||||
|
SELECT id FROM draw_history
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY date DESC
|
||||||
|
LIMIT 100
|
||||||
|
)
|
||||||
|
""", (user_id, user_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 更新成就进度
|
||||||
|
unlocked_achievements = self.update_achievement_progress(user_id, rarity)
|
||||||
|
return unlocked_achievements
|
||||||
307
danding_bot/plugins/onmyoji_gacha/gacha.py
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
import random
|
||||||
|
from typing import Dict, Tuple, List, Optional
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .data_manager import DataManager
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
data_manager = DataManager()
|
||||||
|
|
||||||
|
class GachaSystem:
|
||||||
|
def __init__(self):
|
||||||
|
self.data_manager = data_manager
|
||||||
|
|
||||||
|
def draw(self, user_id: str) -> Dict:
|
||||||
|
"""执行一次抽卡"""
|
||||||
|
# 检查抽卡限制
|
||||||
|
if not self.data_manager.check_daily_limit(user_id):
|
||||||
|
draws_left = self.data_manager.get_draws_left(user_id)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"您今日的抽卡次数已用完,每日限制{config.DAILY_LIMIT}次,明天再来吧!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 抽取稀有度(传递用户ID)
|
||||||
|
rarity = self._draw_rarity(user_id)
|
||||||
|
|
||||||
|
# 从该稀有度中抽取式神
|
||||||
|
shikigami_data = self.data_manager.shikigami_data.get(rarity, [])
|
||||||
|
if not shikigami_data:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"系统错误:{rarity}稀有度下没有可用式神"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 随机选择式神
|
||||||
|
shikigami = random.choice(shikigami_data)
|
||||||
|
|
||||||
|
# 记录抽卡
|
||||||
|
unlocked_achievements = self.data_manager.record_draw(user_id, rarity, shikigami["name"])
|
||||||
|
|
||||||
|
# 剩余次数
|
||||||
|
draws_left = self.data_manager.get_draws_left(user_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"rarity": rarity,
|
||||||
|
"name": shikigami["name"],
|
||||||
|
"image_url": shikigami["image_url"],
|
||||||
|
"draws_left": draws_left,
|
||||||
|
"unlocked_achievements": unlocked_achievements
|
||||||
|
}
|
||||||
|
|
||||||
|
def _draw_rarity(self, user_id: str = None) -> str:
|
||||||
|
"""按概率抽取稀有度"""
|
||||||
|
# 检查是否是特殊概率用户
|
||||||
|
if user_id and user_id in config.SPECIAL_PROBABILITY_USERS:
|
||||||
|
# 100%概率抽到SSR或SP,随机选择
|
||||||
|
return random.choice(["SSR", "SP"])
|
||||||
|
|
||||||
|
# 普通用户的概率逻辑
|
||||||
|
r = random.random() * 100 # 0-100的随机数
|
||||||
|
|
||||||
|
cumulative = 0
|
||||||
|
for rarity, prob in config.RARITY_PROBABILITY.items():
|
||||||
|
cumulative += prob
|
||||||
|
if r < cumulative:
|
||||||
|
return rarity
|
||||||
|
|
||||||
|
# 默认返回R,理论上不会执行到这里
|
||||||
|
return "R"
|
||||||
|
|
||||||
|
def get_user_stats(self, user_id: str) -> Dict:
|
||||||
|
"""获取用户抽卡统计"""
|
||||||
|
user_stats = self.data_manager.get_user_stats()
|
||||||
|
|
||||||
|
if user_id not in user_stats:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "您还没有抽卡记录哦!"
|
||||||
|
}
|
||||||
|
|
||||||
|
stats = user_stats[user_id]
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"total_draws": stats["total_draws"],
|
||||||
|
"R_count": stats["R_count"],
|
||||||
|
"SR_count": stats["SR_count"],
|
||||||
|
"SSR_count": stats["SSR_count"],
|
||||||
|
"SP_count": stats["SP_count"],
|
||||||
|
"recent_draws": stats["draw_history"][-5:] if stats["draw_history"] else []
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_probability_text(self) -> str:
|
||||||
|
"""获取概率展示文本"""
|
||||||
|
probs = config.RARITY_PROBABILITY
|
||||||
|
return f"--- 系统概率 ---\nR: {probs['R']}% | SR: {probs['SR']}% | SSR: {probs['SSR']}% | SP: {probs['SP']}%"
|
||||||
|
|
||||||
|
def get_rank_list(self) -> List[Tuple[str, Dict[str, int]]]:
|
||||||
|
"""获取抽卡排行榜数据"""
|
||||||
|
user_stats = self.data_manager.get_user_stats()
|
||||||
|
|
||||||
|
# 过滤有SSR/SP记录的用户
|
||||||
|
ranked_users = [
|
||||||
|
(user_id, stats)
|
||||||
|
for user_id, stats in user_stats.items()
|
||||||
|
if stats.get("SSR_count", 0) > 0 or stats.get("SP_count", 0) > 0
|
||||||
|
]
|
||||||
|
|
||||||
|
# 按SSR+SP总数降序排序
|
||||||
|
ranked_users.sort(
|
||||||
|
key=lambda x: (x[1].get("SSR_count", 0) + x[1].get("SP_count", 0)),
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return ranked_users
|
||||||
|
|
||||||
|
def get_daily_stats(self) -> Dict:
|
||||||
|
"""获取今日抽卡统计"""
|
||||||
|
daily_draws = self.data_manager.get_daily_draws()
|
||||||
|
today = self.data_manager.get_today_date()
|
||||||
|
|
||||||
|
if not daily_draws or today not in daily_draws:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "今日还没有人抽卡哦!"
|
||||||
|
}
|
||||||
|
|
||||||
|
today_stats = daily_draws[today]
|
||||||
|
total_stats = {
|
||||||
|
"total_users": len(today_stats),
|
||||||
|
"total_draws": 0,
|
||||||
|
"R_count": 0,
|
||||||
|
"SR_count": 0,
|
||||||
|
"SSR_count": 0,
|
||||||
|
"SP_count": 0,
|
||||||
|
"user_stats": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# 统计每个用户的抽卡情况
|
||||||
|
for user_id, draws in today_stats.items():
|
||||||
|
user_stats = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"total_draws": len(draws),
|
||||||
|
"R_count": sum(1 for d in draws if d["rarity"] == "R"),
|
||||||
|
"SR_count": sum(1 for d in draws if d["rarity"] == "SR"),
|
||||||
|
"SSR_count": sum(1 for d in draws if d["rarity"] == "SSR"),
|
||||||
|
"SP_count": sum(1 for d in draws if d["rarity"] == "SP")
|
||||||
|
}
|
||||||
|
|
||||||
|
# 更新总统计
|
||||||
|
total_stats["total_draws"] += user_stats["total_draws"]
|
||||||
|
total_stats["R_count"] += user_stats["R_count"]
|
||||||
|
total_stats["SR_count"] += user_stats["SR_count"]
|
||||||
|
total_stats["SSR_count"] += user_stats["SSR_count"]
|
||||||
|
total_stats["SP_count"] += user_stats["SP_count"]
|
||||||
|
|
||||||
|
# 只记录抽到SSR或SP的用户
|
||||||
|
if user_stats["SSR_count"] > 0 or user_stats["SP_count"] > 0:
|
||||||
|
total_stats["user_stats"].append(user_stats)
|
||||||
|
|
||||||
|
# 按SSR+SP数量排序用户统计
|
||||||
|
total_stats["user_stats"].sort(
|
||||||
|
key=lambda x: (x["SSR_count"] + x["SP_count"]),
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构建稀有度统计
|
||||||
|
rarity_stats = {
|
||||||
|
"R": total_stats["R_count"],
|
||||||
|
"SR": total_stats["SR_count"],
|
||||||
|
"SSR": total_stats["SSR_count"],
|
||||||
|
"SP": total_stats["SP_count"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建排行榜数据
|
||||||
|
top_users = []
|
||||||
|
for user_stat in total_stats["user_stats"]:
|
||||||
|
top_users.append({
|
||||||
|
"user_id": user_stat["user_id"],
|
||||||
|
"ssr_count": user_stat["SSR_count"] + user_stat["SP_count"]
|
||||||
|
})
|
||||||
|
|
||||||
|
final_stats = {
|
||||||
|
"total_users": total_stats["total_users"],
|
||||||
|
"total_draws": total_stats["total_draws"],
|
||||||
|
"rarity_stats": rarity_stats,
|
||||||
|
"top_users": top_users
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"date": today,
|
||||||
|
"stats": final_stats
|
||||||
|
}
|
||||||
|
|
||||||
|
def triple_draw(self, user_id: str) -> Dict:
|
||||||
|
"""执行三连抽"""
|
||||||
|
# 检查是否有足够的抽卡次数
|
||||||
|
draws_left = self.data_manager.get_draws_left(user_id)
|
||||||
|
if draws_left < 3:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"抽卡次数不足,您今日还剩{draws_left}次抽卡机会,三连抽需要3次机会"
|
||||||
|
}
|
||||||
|
|
||||||
|
results = []
|
||||||
|
all_unlocked_achievements = []
|
||||||
|
|
||||||
|
# 执行三次抽卡
|
||||||
|
for i in range(3):
|
||||||
|
# 抽取稀有度(传递用户ID)
|
||||||
|
rarity = self._draw_rarity(user_id)
|
||||||
|
|
||||||
|
# 从该稀有度中抽取式神
|
||||||
|
shikigami_data = self.data_manager.shikigami_data.get(rarity, [])
|
||||||
|
if not shikigami_data:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"系统错误:{rarity}稀有度下没有可用式神"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 随机选择式神
|
||||||
|
shikigami = random.choice(shikigami_data)
|
||||||
|
|
||||||
|
# 记录抽卡
|
||||||
|
unlocked_achievements = self.data_manager.record_draw(user_id, rarity, shikigami["name"])
|
||||||
|
all_unlocked_achievements.extend(unlocked_achievements)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"rarity": rarity,
|
||||||
|
"name": shikigami["name"],
|
||||||
|
"image_url": shikigami["image_url"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 剩余次数
|
||||||
|
draws_left = self.data_manager.get_draws_left(user_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"results": results,
|
||||||
|
"draws_left": draws_left,
|
||||||
|
"unlocked_achievements": list(set(all_unlocked_achievements)) # 去重
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_user_achievements(self, user_id: str) -> Dict:
|
||||||
|
"""获取用户成就信息"""
|
||||||
|
achievement_data = self.data_manager.get_user_achievements(user_id)
|
||||||
|
|
||||||
|
if not achievement_data["unlocked"] and all(v == 0 for v in achievement_data["progress"].values()):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "您还没有任何成就进度哦!快去抽卡吧!"
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"achievements": achievement_data["unlocked"],
|
||||||
|
"progress": achievement_data["progress"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_daily_detailed_records(self, date: Optional[str] = None) -> Dict:
|
||||||
|
"""获取每日详细抽卡记录"""
|
||||||
|
if not date:
|
||||||
|
date = self.data_manager.get_today_date()
|
||||||
|
|
||||||
|
daily_draws = self.data_manager.get_daily_draws()
|
||||||
|
|
||||||
|
if not daily_draws or date not in daily_draws:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"{date} 没有抽卡记录"
|
||||||
|
}
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for user_id, draws in daily_draws[date].items():
|
||||||
|
for draw in draws:
|
||||||
|
# 检查这次抽卡是否解锁了成就
|
||||||
|
unlocked_achievements = []
|
||||||
|
draw_time = draw.get("timestamp", "未知时间")
|
||||||
|
|
||||||
|
# 获取用户成就信息
|
||||||
|
achievement_data = self.data_manager.get_user_achievements(user_id)
|
||||||
|
if achievement_data["unlocked"]:
|
||||||
|
# 检查是否有在抽卡时间之后解锁的成就
|
||||||
|
for achievement_id, achievement_info in achievement_data["unlocked"].items():
|
||||||
|
if achievement_info["unlocked_date"] == f"{date} {draw_time}":
|
||||||
|
unlocked_achievements.append(achievement_id)
|
||||||
|
|
||||||
|
records.append({
|
||||||
|
"user_id": user_id,
|
||||||
|
"draw_time": draw_time,
|
||||||
|
"shikigami_name": draw["name"],
|
||||||
|
"rarity": draw["rarity"],
|
||||||
|
"unlocked_achievements": unlocked_achievements
|
||||||
|
})
|
||||||
|
|
||||||
|
# 按时间排序
|
||||||
|
records.sort(key=lambda x: x["draw_time"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"date": date,
|
||||||
|
"records": records,
|
||||||
|
"total_count": len(records)
|
||||||
|
}
|
||||||
799
danding_bot/plugins/onmyoji_gacha/templates/admin.html
Normal file
@@ -0,0 +1,799 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>阴阳师抽卡系统 - 管理后台</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||||
|
<style>
|
||||||
|
.stat-card {
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
.rarity-r { color: #6c757d; }
|
||||||
|
.rarity-sr { color: #0d6efd; }
|
||||||
|
.rarity-ssr { color: #ffc107; }
|
||||||
|
.rarity-sp { color: #dc3545; }
|
||||||
|
.achievement-card {
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.table th {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="#">
|
||||||
|
<i class="bi bi-dice-5"></i> 阴阳师抽卡系统 - 管理后台
|
||||||
|
</a>
|
||||||
|
<div class="ms-auto">
|
||||||
|
<span class="navbar-text">
|
||||||
|
<i class="bi bi-shield-lock"></i> 管理员访问
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<!-- 统计概览 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card bg-light">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title"><i class="bi bi-people"></i> 参与人数</h5>
|
||||||
|
<h2 class="text-primary" id="totalUsers">-</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card bg-light">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title"><i class="bi bi-dice-6"></i> 总抽卡次数</h5>
|
||||||
|
<h2 class="text-success" id="totalDraws">-</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card bg-light">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title"><i class="bi bi-trophy"></i> SSR/SP总数</h5>
|
||||||
|
<h2 class="text-warning" id="totalSSRSP">-</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card stat-card bg-light">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title"><i class="bi bi-percent"></i> SSR/SP概率</h5>
|
||||||
|
<h2 class="text-danger" id="ssrSpRate">-</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 稀有度分布 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="bi bi-pie-chart"></i> 稀有度分布</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="rarity-r">R</h3>
|
||||||
|
<div class="progress mb-2">
|
||||||
|
<div class="progress-bar bg-secondary" id="rRate" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<h4 id="rCount">-</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="rarity-sr">SR</h3>
|
||||||
|
<div class="progress mb-2">
|
||||||
|
<div class="progress-bar bg-primary" id="srRate" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<h4 id="srCount">-</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="rarity-ssr">SSR</h3>
|
||||||
|
<div class="progress mb-2">
|
||||||
|
<div class="progress-bar bg-warning" id="ssrRate" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<h4 id="ssrCount">-</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="rarity-sp">SP</h3>
|
||||||
|
<div class="progress mb-2">
|
||||||
|
<div class="progress-bar bg-danger" id="spRate" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<h4 id="spCount">-</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 排行榜 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5><i class="bi bi-trophy"></i> SSR/SP排行榜</h5>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="refreshRankList()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> 刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>排名</th>
|
||||||
|
<th>用户ID</th>
|
||||||
|
<th>总抽卡次数</th>
|
||||||
|
<th class="rarity-r">R</th>
|
||||||
|
<th class="rarity-sr">SR</th>
|
||||||
|
<th class="rarity-ssr">SSR</th>
|
||||||
|
<th class="rarity-sp">SP</th>
|
||||||
|
<th>SSR/SP总数</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="rankListBody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center">加载中...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户查询 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="bi bi-person-search"></i> 用户查询</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" id="userIdInput" placeholder="输入用户ID">
|
||||||
|
<button class="btn btn-primary" onclick="queryUser()">
|
||||||
|
<i class="bi bi-search"></i> 查询
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="userStatsResult" style="display: none;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>抽卡统计</h6>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<tr>
|
||||||
|
<td>总抽卡次数</td>
|
||||||
|
<td id="userTotalDraws">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="rarity-r">R卡数量</td>
|
||||||
|
<td id="userRCount">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="rarity-sr">SR卡数量</td>
|
||||||
|
<td id="userSRCount">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="rarity-ssr">SSR卡数量</td>
|
||||||
|
<td id="userSSRCount">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="rarity-sp">SP卡数量</td>
|
||||||
|
<td id="userSPCount">-</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>最近抽卡记录</h6>
|
||||||
|
<div id="recentDraws">
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 成就查询 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="bi bi-award"></i> 成就查询</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" id="achievementUserIdInput" placeholder="输入用户ID">
|
||||||
|
<button class="btn btn-primary" onclick="queryAchievements()">
|
||||||
|
<i class="bi bi-search"></i> 查询
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="achievementResult" style="display: none;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>已解锁成就</h6>
|
||||||
|
<div id="unlockedAchievements">
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>成就进度</h6>
|
||||||
|
<div class="achievement-card">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>连续抽卡天数</span>
|
||||||
|
<span id="consecutiveDays">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress mt-2">
|
||||||
|
<div class="progress-bar" id="consecutiveDaysProgress" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-card">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>无SSR/SP连击</span>
|
||||||
|
<span id="noSsrStreak">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress mt-2">
|
||||||
|
<div class="progress-bar bg-danger" id="noSsrStreakProgress" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 每日详细抽卡记录 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5><i class="bi bi-table"></i> 今日抽卡详细记录</h5>
|
||||||
|
<div>
|
||||||
|
<input type="date" class="form-control form-control-sm d-inline-block me-2" id="recordDateInput" style="width: auto;">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="loadDailyRecords()">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> 刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>抽卡时间</th>
|
||||||
|
<th>用户ID</th>
|
||||||
|
<th>式神名称</th>
|
||||||
|
<th>稀有度</th>
|
||||||
|
<th>成就解锁</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="dailyRecordsBody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center">加载中...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<div>
|
||||||
|
<span>总记录数: <strong id="totalRecordsCount">0</strong></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination pagination-sm" id="recordsPagination">
|
||||||
|
<!-- 分页控件将通过JS动态生成 -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="bg-light text-center py-3 mt-5">
|
||||||
|
<div class="container">
|
||||||
|
<p class="mb-0">阴阳师抽卡系统 - 管理后台 © 2024</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// API配置
|
||||||
|
const API_BASE = '/onmyoji_gacha/api';
|
||||||
|
let ADMIN_TOKEN = localStorage.getItem('adminToken');
|
||||||
|
|
||||||
|
// 令牌重置函数
|
||||||
|
function resetToken() {
|
||||||
|
localStorage.removeItem('adminToken');
|
||||||
|
const newToken = prompt('请输入管理员令牌:');
|
||||||
|
if (newToken) {
|
||||||
|
ADMIN_TOKEN = newToken;
|
||||||
|
localStorage.setItem('adminToken', ADMIN_TOKEN);
|
||||||
|
// 更新请求头
|
||||||
|
headers.Authorization = `Bearer ${ADMIN_TOKEN}`;
|
||||||
|
console.log('令牌已重置:', ADMIN_TOKEN);
|
||||||
|
// 重新加载数据
|
||||||
|
loadDailyStats();
|
||||||
|
loadRankList();
|
||||||
|
loadDailyRecords();
|
||||||
|
} else {
|
||||||
|
alert('需要管理员令牌才能访问');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有保存的令牌,提示输入
|
||||||
|
if (!ADMIN_TOKEN) {
|
||||||
|
ADMIN_TOKEN = prompt('请输入管理员令牌:');
|
||||||
|
if (ADMIN_TOKEN) {
|
||||||
|
localStorage.setItem('adminToken', ADMIN_TOKEN);
|
||||||
|
} else {
|
||||||
|
alert('需要管理员令牌才能访问');
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('使用的管理员令牌:', ADMIN_TOKEN);
|
||||||
|
|
||||||
|
// API请求头
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('请求头:', headers);
|
||||||
|
|
||||||
|
// 添加令牌重置按钮到导航栏
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const navbar = document.querySelector('.navbar .ms-auto');
|
||||||
|
const resetButton = document.createElement('button');
|
||||||
|
resetButton.className = 'btn btn-outline-light btn-sm ms-2';
|
||||||
|
resetButton.innerHTML = '<i class="bi bi-key"></i> 重置令牌';
|
||||||
|
resetButton.onclick = resetToken;
|
||||||
|
navbar.appendChild(resetButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面加载时获取数据
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadDailyStats();
|
||||||
|
loadRankList();
|
||||||
|
// 设置今天的日期为默认值
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
document.getElementById('recordDateInput').value = today;
|
||||||
|
loadDailyRecords();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载今日统计
|
||||||
|
async function loadDailyStats() {
|
||||||
|
try {
|
||||||
|
console.log('正在请求每日统计...');
|
||||||
|
const response = await fetch(`${API_BASE}/stats/daily`, { headers });
|
||||||
|
console.log('响应状态:', response.status);
|
||||||
|
console.log('响应头:', response.headers);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('API 错误响应:', errorText);
|
||||||
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('每日统计数据:', data);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const stats = data.stats;
|
||||||
|
document.getElementById('totalUsers').textContent = stats.total_users;
|
||||||
|
document.getElementById('totalDraws').textContent = stats.total_draws;
|
||||||
|
|
||||||
|
const ssrSpTotal = stats.rarity_stats.SSR + stats.rarity_stats.SP;
|
||||||
|
document.getElementById('totalSSRSP').textContent = ssrSpTotal;
|
||||||
|
|
||||||
|
const ssrSpRate = ((ssrSpTotal / stats.total_draws) * 100).toFixed(2);
|
||||||
|
document.getElementById('ssrSpRate').textContent = ssrSpRate + '%';
|
||||||
|
|
||||||
|
// 更新稀有度分布
|
||||||
|
updateRarityDistribution(stats.rarity_stats, stats.total_draws);
|
||||||
|
} else {
|
||||||
|
console.error('API 返回失败:', data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载统计数据失败:', error);
|
||||||
|
// 显示错误信息给用户
|
||||||
|
document.getElementById('totalUsers').textContent = '错误';
|
||||||
|
document.getElementById('totalDraws').textContent = '错误';
|
||||||
|
document.getElementById('totalSSRSP').textContent = '错误';
|
||||||
|
document.getElementById('ssrSpRate').textContent = '错误';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新稀有度分布
|
||||||
|
function updateRarityDistribution(rarityStats, totalDraws) {
|
||||||
|
const rarities = ['R', 'SR', 'SSR', 'SP'];
|
||||||
|
|
||||||
|
rarities.forEach(rarity => {
|
||||||
|
const count = rarityStats[rarity];
|
||||||
|
const rate = totalDraws > 0 ? (count / totalDraws * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
document.getElementById(`${rarity.toLowerCase()}Count`).textContent = count;
|
||||||
|
document.getElementById(`${rarity.toLowerCase()}Rate`).style.width = rate + '%';
|
||||||
|
document.getElementById(`${rarity.toLowerCase()}Rate`).textContent = rate + '%';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载排行榜
|
||||||
|
async function loadRankList() {
|
||||||
|
try {
|
||||||
|
console.log('正在请求排行榜数据...');
|
||||||
|
const response = await fetch(`${API_BASE}/stats/rank`, { headers });
|
||||||
|
console.log('排行榜响应状态:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('排行榜 API 错误响应:', errorText);
|
||||||
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('排行榜数据:', data);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const tbody = document.getElementById('rankListBody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
data.data.forEach((user, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>${user.user_id}</td>
|
||||||
|
<td>${user.total_draws}</td>
|
||||||
|
<td class="rarity-r">${user.R_count}</td>
|
||||||
|
<td class="rarity-sr">${user.SR_count}</td>
|
||||||
|
<td class="rarity-ssr">${user.SSR_count}</td>
|
||||||
|
<td class="rarity-sp">${user.SP_count}</td>
|
||||||
|
<td><strong>${user.ssr_sp_total}</strong></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="viewUserDetails('${user.user_id}')">
|
||||||
|
<i class="bi bi-eye"></i> 详情
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="9" class="text-center">暂无数据</td></tr>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('排行榜 API 返回失败:', data);
|
||||||
|
document.getElementById('rankListBody').innerHTML = '<tr><td colspan="9" class="text-center">数据加载失败</td></tr>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载排行榜失败:', error);
|
||||||
|
document.getElementById('rankListBody').innerHTML = '<tr><td colspan="9" class="text-center">加载失败,请检查令牌</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新排行榜
|
||||||
|
function refreshRankList() {
|
||||||
|
loadRankList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户
|
||||||
|
async function queryUser() {
|
||||||
|
const userId = document.getElementById('userIdInput').value.trim();
|
||||||
|
if (!userId) {
|
||||||
|
alert('请输入用户ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/stats/user/${userId}`, { headers });
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('userStatsResult').style.display = 'block';
|
||||||
|
|
||||||
|
document.getElementById('userTotalDraws').textContent = data.total_draws;
|
||||||
|
document.getElementById('userRCount').textContent = data.R_count;
|
||||||
|
document.getElementById('userSRCount').textContent = data.SR_count;
|
||||||
|
document.getElementById('userSSRCount').textContent = data.SSR_count;
|
||||||
|
document.getElementById('userSPCount').textContent = data.SP_count;
|
||||||
|
|
||||||
|
// 显示最近抽卡记录
|
||||||
|
const recentDrawsDiv = document.getElementById('recentDraws');
|
||||||
|
if (data.recent_draws && data.recent_draws.length > 0) {
|
||||||
|
recentDrawsDiv.innerHTML = data.recent_draws.map(draw =>
|
||||||
|
`<div class="mb-1">
|
||||||
|
<span class="badge bg-secondary">${draw.date}</span>
|
||||||
|
<span class="badge rarity-${draw.rarity.toLowerCase()}">${draw.rarity}</span>
|
||||||
|
${draw.name}
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
} else {
|
||||||
|
recentDrawsDiv.innerHTML = '<p class="text-muted">暂无抽卡记录</p>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('未找到用户数据');
|
||||||
|
document.getElementById('userStatsResult').style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询用户失败:', error);
|
||||||
|
alert('查询失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看用户详情
|
||||||
|
function viewUserDetails(userId) {
|
||||||
|
document.getElementById('userIdInput').value = userId;
|
||||||
|
queryUser();
|
||||||
|
document.getElementById('achievementUserIdInput').value = userId;
|
||||||
|
queryAchievements();
|
||||||
|
|
||||||
|
// 滚动到用户查询区域
|
||||||
|
document.querySelector('.card:has(#userIdInput)').scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询成就
|
||||||
|
async function queryAchievements() {
|
||||||
|
const userId = document.getElementById('achievementUserIdInput').value.trim();
|
||||||
|
if (!userId) {
|
||||||
|
alert('请输入用户ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/achievements/${userId}`, { headers });
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('achievementResult').style.display = 'block';
|
||||||
|
|
||||||
|
// 显示已解锁成就
|
||||||
|
const unlockedDiv = document.getElementById('unlockedAchievements');
|
||||||
|
const achievements = Object.entries(data.achievements);
|
||||||
|
|
||||||
|
if (achievements.length > 0) {
|
||||||
|
unlockedDiv.innerHTML = achievements.map(([id, info]) =>
|
||||||
|
`<div class="achievement-card">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span>${id}</span>
|
||||||
|
<span class="badge bg-success">已解锁</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">${info.unlocked_date}</small>
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
} else {
|
||||||
|
unlockedDiv.innerHTML = '<p class="text-muted">暂无已解锁成就</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示成就进度
|
||||||
|
const progress = data.progress;
|
||||||
|
document.getElementById('consecutiveDays').textContent = progress.consecutive_days + ' 天';
|
||||||
|
document.getElementById('noSsrStreak').textContent = progress.no_ssr_streak + ' 次';
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
const consecutiveProgress = Math.min((progress.consecutive_days / 150) * 100, 100);
|
||||||
|
document.getElementById('consecutiveDaysProgress').style.width = consecutiveProgress + '%';
|
||||||
|
|
||||||
|
const noSsrProgress = Math.min((progress.no_ssr_streak / 180) * 100, 100);
|
||||||
|
document.getElementById('noSsrStreakProgress').style.width = noSsrProgress + '%';
|
||||||
|
} else {
|
||||||
|
alert('未找到用户成就数据');
|
||||||
|
document.getElementById('achievementResult').style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询成就失败:', error);
|
||||||
|
alert('查询失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每页记录数
|
||||||
|
const RECORDS_PER_PAGE = 20;
|
||||||
|
let currentRecords = [];
|
||||||
|
let currentPage = 1;
|
||||||
|
|
||||||
|
// 加载每日详细抽卡记录
|
||||||
|
async function loadDailyRecords() {
|
||||||
|
const date = document.getElementById('recordDateInput').value;
|
||||||
|
if (!date) {
|
||||||
|
alert('请选择日期');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('正在请求每日详细抽卡记录...', date);
|
||||||
|
const response = await fetch(`${API_BASE}/records/daily?date=${date}`, { headers });
|
||||||
|
console.log('每日详细记录响应状态:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('每日详细记录 API 错误响应:', errorText);
|
||||||
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('每日详细记录数据:', data);
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
currentRecords = data.records;
|
||||||
|
currentPage = 1;
|
||||||
|
document.getElementById('totalRecordsCount').textContent = data.total_count;
|
||||||
|
displayRecords();
|
||||||
|
} else {
|
||||||
|
console.error('每日详细记录 API 返回失败:', data);
|
||||||
|
document.getElementById('dailyRecordsBody').innerHTML = '<tr><td colspan="5" class="text-center">暂无数据</td></tr>';
|
||||||
|
document.getElementById('totalRecordsCount').textContent = '0';
|
||||||
|
document.getElementById('recordsPagination').innerHTML = '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载每日详细记录失败:', error);
|
||||||
|
document.getElementById('dailyRecordsBody').innerHTML = '<tr><td colspan="5" class="text-center">加载失败,请检查令牌</td></tr>';
|
||||||
|
document.getElementById('totalRecordsCount').textContent = '0';
|
||||||
|
document.getElementById('recordsPagination').innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示记录(支持分页)
|
||||||
|
function displayRecords() {
|
||||||
|
const tbody = document.getElementById('dailyRecordsBody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
// 计算分页
|
||||||
|
const startIndex = (currentPage - 1) * RECORDS_PER_PAGE;
|
||||||
|
const endIndex = Math.min(startIndex + RECORDS_PER_PAGE, currentRecords.length);
|
||||||
|
const pageRecords = currentRecords.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
if (pageRecords.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center">暂无数据</td></tr>';
|
||||||
|
document.getElementById('recordsPagination').innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示当前页的记录
|
||||||
|
pageRecords.forEach(record => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
// 格式化成就解锁信息
|
||||||
|
let achievementsHtml = '';
|
||||||
|
if (record.unlocked_achievements && record.unlocked_achievements.length > 0) {
|
||||||
|
achievementsHtml = record.unlocked_achievements.map(achievement =>
|
||||||
|
`<span class="badge bg-success me-1">${achievement}</span>`
|
||||||
|
).join('');
|
||||||
|
} else {
|
||||||
|
achievementsHtml = '<span class="text-muted">无</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${record.draw_time}</td>
|
||||||
|
<td>${record.user_id}</td>
|
||||||
|
<td>${record.shikigami_name}</td>
|
||||||
|
<td><span class="badge rarity-${record.rarity.toLowerCase()}">${record.rarity}</span></td>
|
||||||
|
<td>${achievementsHtml}</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新分页控件
|
||||||
|
updatePagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新分页控件
|
||||||
|
function updatePagination() {
|
||||||
|
const totalPages = Math.ceil(currentRecords.length / RECORDS_PER_PAGE);
|
||||||
|
const pagination = document.getElementById('recordsPagination');
|
||||||
|
pagination.innerHTML = '';
|
||||||
|
|
||||||
|
if (totalPages <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上一页按钮
|
||||||
|
const prevLi = document.createElement('li');
|
||||||
|
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
|
||||||
|
prevLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage - 1})">上一页</a>`;
|
||||||
|
pagination.appendChild(prevLi);
|
||||||
|
|
||||||
|
// 页码按钮
|
||||||
|
const maxVisiblePages = 5;
|
||||||
|
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||||
|
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||||
|
|
||||||
|
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||||
|
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPage > 1) {
|
||||||
|
const firstLi = document.createElement('li');
|
||||||
|
firstLi.className = 'page-item';
|
||||||
|
firstLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(1)">1</a>`;
|
||||||
|
pagination.appendChild(firstLi);
|
||||||
|
|
||||||
|
if (startPage > 2) {
|
||||||
|
const ellipsisLi = document.createElement('li');
|
||||||
|
ellipsisLi.className = 'page-item disabled';
|
||||||
|
ellipsisLi.innerHTML = `<a class="page-link" href="#">...</a>`;
|
||||||
|
pagination.appendChild(ellipsisLi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
const pageLi = document.createElement('li');
|
||||||
|
pageLi.className = `page-item ${i === currentPage ? 'active' : ''}`;
|
||||||
|
pageLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${i})">${i}</a>`;
|
||||||
|
pagination.appendChild(pageLi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage < totalPages) {
|
||||||
|
if (endPage < totalPages - 1) {
|
||||||
|
const ellipsisLi = document.createElement('li');
|
||||||
|
ellipsisLi.className = 'page-item disabled';
|
||||||
|
ellipsisLi.innerHTML = `<a class="page-link" href="#">...</a>`;
|
||||||
|
pagination.appendChild(ellipsisLi);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastLi = document.createElement('li');
|
||||||
|
lastLi.className = 'page-item';
|
||||||
|
lastLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${totalPages})">${totalPages}</a>`;
|
||||||
|
pagination.appendChild(lastLi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一页按钮
|
||||||
|
const nextLi = document.createElement('li');
|
||||||
|
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
||||||
|
nextLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage + 1})">下一页</a>`;
|
||||||
|
pagination.appendChild(nextLi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换页面
|
||||||
|
function changePage(page) {
|
||||||
|
const totalPages = Math.ceil(currentRecords.length / RECORDS_PER_PAGE);
|
||||||
|
if (page < 1 || page > totalPages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentPage = page;
|
||||||
|
displayRecords();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
danding_bot/plugins/onmyoji_gacha/utils.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import os
|
||||||
|
from typing import Union, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def get_image_path(file_path: str) -> str:
|
||||||
|
"""获取图片的绝对路径"""
|
||||||
|
return os.path.abspath(file_path)
|
||||||
|
|
||||||
|
def format_user_mention(user_id: str, user_name: Optional[str] = None) -> str:
|
||||||
|
"""格式化用户@信息"""
|
||||||
|
display_name = user_name if user_name else f"用户{user_id}"
|
||||||
|
return f"@{display_name}"
|
||||||
202
danding_bot/plugins/onmyoji_gacha/web_api.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
"""
|
||||||
|
onmyoji_gacha 插件的 Web API 接口
|
||||||
|
使用 NoneBot 内置的 FastAPI 适配器提供管理员后台接口
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Header, Request
|
||||||
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from nonebot import get_driver
|
||||||
|
from .config import Config
|
||||||
|
from .gacha import GachaSystem
|
||||||
|
|
||||||
|
# 创建配置实例
|
||||||
|
config = Config()
|
||||||
|
gacha_system = GachaSystem()
|
||||||
|
|
||||||
|
# 创建 FastAPI 路由
|
||||||
|
router = APIRouter(prefix="/onmyoji_gacha", tags=["onmyoji_gacha"])
|
||||||
|
|
||||||
|
# 设置模板目录
|
||||||
|
templates = Jinja2Templates(directory="danding_bot/plugins/onmyoji_gacha/templates")
|
||||||
|
|
||||||
|
# 依赖:验证管理员权限
|
||||||
|
async def verify_admin_token(authorization: Optional[str] = Header(None)):
|
||||||
|
"""验证管理员权限"""
|
||||||
|
print(f"🔐 验证管理员令牌: {authorization}")
|
||||||
|
|
||||||
|
if not authorization:
|
||||||
|
print("❌ 缺少认证令牌")
|
||||||
|
raise HTTPException(status_code=401, detail="缺少认证令牌")
|
||||||
|
|
||||||
|
token = authorization.replace("Bearer ", "")
|
||||||
|
print(f"🔑 提取的令牌: {token}")
|
||||||
|
print(f"🎯 期望的令牌: {config.WEB_ADMIN_TOKEN}")
|
||||||
|
|
||||||
|
if token != config.WEB_ADMIN_TOKEN:
|
||||||
|
print("❌ 令牌验证失败")
|
||||||
|
raise HTTPException(status_code=403, detail="无效的认证令牌")
|
||||||
|
|
||||||
|
print("✅ 令牌验证成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# API 响应模型
|
||||||
|
class DailyStatsResponse(BaseModel):
|
||||||
|
success: bool
|
||||||
|
date: str
|
||||||
|
stats: Dict[str, Any]
|
||||||
|
|
||||||
|
class UserStatsResponse(BaseModel):
|
||||||
|
success: bool
|
||||||
|
user_id: str
|
||||||
|
total_draws: int
|
||||||
|
R_count: int
|
||||||
|
SR_count: int
|
||||||
|
SSR_count: int
|
||||||
|
SP_count: int
|
||||||
|
recent_draws: List[Dict[str, str]]
|
||||||
|
|
||||||
|
class RankListResponse(BaseModel):
|
||||||
|
success: bool
|
||||||
|
data: List[Dict[str, Any]]
|
||||||
|
|
||||||
|
class AchievementResponse(BaseModel):
|
||||||
|
success: bool
|
||||||
|
user_id: str
|
||||||
|
achievements: Dict[str, Any]
|
||||||
|
progress: Dict[str, Any]
|
||||||
|
|
||||||
|
class DailyDetailedRecordsResponse(BaseModel):
|
||||||
|
success: bool
|
||||||
|
date: str
|
||||||
|
records: List[Dict[str, Any]]
|
||||||
|
total_count: int
|
||||||
|
|
||||||
|
# 管理后台页面
|
||||||
|
@router.get("/admin", response_class=HTMLResponse)
|
||||||
|
async def admin_page(request: Request):
|
||||||
|
"""管理后台页面"""
|
||||||
|
return templates.TemplateResponse("admin.html", {"request": request})
|
||||||
|
|
||||||
|
# API 端点
|
||||||
|
@router.get("/api/stats/daily", response_model=DailyStatsResponse, dependencies=[Depends(verify_admin_token)])
|
||||||
|
async def get_daily_stats():
|
||||||
|
"""获取今日抽卡统计"""
|
||||||
|
result = gacha_system.get_daily_stats()
|
||||||
|
if not result["success"]:
|
||||||
|
return result
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"date": result["date"],
|
||||||
|
"stats": result["stats"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/api/stats/user/{user_id}", response_model=UserStatsResponse, dependencies=[Depends(verify_admin_token)])
|
||||||
|
async def get_user_stats(user_id: str):
|
||||||
|
"""获取用户抽卡统计"""
|
||||||
|
result = gacha_system.get_user_stats(user_id)
|
||||||
|
if not result["success"]:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"user_id": user_id,
|
||||||
|
"total_draws": 0,
|
||||||
|
"R_count": 0,
|
||||||
|
"SR_count": 0,
|
||||||
|
"SSR_count": 0,
|
||||||
|
"SP_count": 0,
|
||||||
|
"recent_draws": []
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"user_id": user_id,
|
||||||
|
"total_draws": result["total_draws"],
|
||||||
|
"R_count": result["R_count"],
|
||||||
|
"SR_count": result["SR_count"],
|
||||||
|
"SSR_count": result["SSR_count"],
|
||||||
|
"SP_count": result["SP_count"],
|
||||||
|
"recent_draws": result["recent_draws"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/api/stats/rank", response_model=RankListResponse, dependencies=[Depends(verify_admin_token)])
|
||||||
|
async def get_rank_list():
|
||||||
|
"""获取抽卡排行榜"""
|
||||||
|
rank_data = gacha_system.get_rank_list()
|
||||||
|
|
||||||
|
# 转换数据格式
|
||||||
|
formatted_data = []
|
||||||
|
for user_id, stats in rank_data:
|
||||||
|
formatted_data.append({
|
||||||
|
"user_id": user_id,
|
||||||
|
"total_draws": stats["total_draws"],
|
||||||
|
"R_count": stats["R_count"],
|
||||||
|
"SR_count": stats["SR_count"],
|
||||||
|
"SSR_count": stats["SSR_count"],
|
||||||
|
"SP_count": stats["SP_count"],
|
||||||
|
"ssr_sp_total": stats["SSR_count"] + stats["SP_count"]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": formatted_data
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/api/achievements/{user_id}", response_model=AchievementResponse, dependencies=[Depends(verify_admin_token)])
|
||||||
|
async def get_user_achievements(user_id: str):
|
||||||
|
"""获取用户成就信息"""
|
||||||
|
result = gacha_system.get_user_achievements(user_id)
|
||||||
|
if not result["success"]:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"user_id": user_id,
|
||||||
|
"achievements": {},
|
||||||
|
"progress": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"user_id": user_id,
|
||||||
|
"achievements": result["achievements"],
|
||||||
|
"progress": result["progress"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/api/records/daily", response_model=DailyDetailedRecordsResponse, dependencies=[Depends(verify_admin_token)])
|
||||||
|
async def get_daily_detailed_records(date: Optional[str] = None):
|
||||||
|
"""获取每日详细抽卡记录"""
|
||||||
|
result = gacha_system.get_daily_detailed_records(date)
|
||||||
|
if not result["success"]:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"date": date or gacha_system.data_manager.get_today_date(),
|
||||||
|
"records": [],
|
||||||
|
"total_count": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"date": result["date"],
|
||||||
|
"records": result["records"],
|
||||||
|
"total_count": result["total_count"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 注册路由到 NoneBot 的 FastAPI 应用
|
||||||
|
# 将在插件加载时由 __init__.py 调用
|
||||||
|
def register_web_routes():
|
||||||
|
"""注册 Web 路由到 NoneBot 的 FastAPI 应用"""
|
||||||
|
try:
|
||||||
|
from nonebot import get_driver
|
||||||
|
driver = get_driver()
|
||||||
|
# 获取 FastAPI 应用实例
|
||||||
|
app = driver.server_app
|
||||||
|
# 注册路由
|
||||||
|
app.include_router(router)
|
||||||
|
print("✅ onmyoji_gacha Web API 路由注册成功")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 注册 Web 路由时出错: {e}")
|
||||||
|
return False
|
||||||
14
danding_bot/plugins/welcome_plugin/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from nonebot import get_plugin_config
|
||||||
|
from nonebot.plugin import PluginMetadata
|
||||||
|
from . import welcome
|
||||||
|
|
||||||
|
__plugin_meta__ = PluginMetadata(
|
||||||
|
name="welcome_plugin",
|
||||||
|
description="入群欢迎插件",
|
||||||
|
usage="""
|
||||||
|
# 欢迎插件
|
||||||
|
当新用户加入群聊(621016172)时,会自动欢迎并发送帮助菜单
|
||||||
|
|
||||||
|
注意:本插件仅在特定群组中可用
|
||||||
|
""",
|
||||||
|
)
|
||||||
64
danding_bot/plugins/welcome_plugin/welcome.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from nonebot import on_notice, logger
|
||||||
|
from nonebot.typing import T_State
|
||||||
|
from nonebot.adapters.onebot.v11.event import GroupIncreaseNoticeEvent
|
||||||
|
from nonebot.adapters.onebot.v11 import Bot, Message
|
||||||
|
from nonebot_plugin_saa import Text, Image, MessageFactory
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
|
||||||
|
# 定义用于过滤目标群的规则函数
|
||||||
|
async def rule_fun(event: GroupIncreaseNoticeEvent):
|
||||||
|
id = event.group_id
|
||||||
|
if id in [621016172]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 监听群成员增加事件
|
||||||
|
group_welcome = on_notice(rule=rule_fun, priority=1, block=True)
|
||||||
|
|
||||||
|
@group_welcome.handle()
|
||||||
|
async def handle_group_increase(bot: Bot, event: GroupIncreaseNoticeEvent, state: T_State):
|
||||||
|
"""处理群成员增加事件,发送欢迎消息和帮助菜单"""
|
||||||
|
# 获取新成员的用户ID
|
||||||
|
user_id = event.get_user_id()
|
||||||
|
|
||||||
|
# 构建欢迎消息文本
|
||||||
|
welcome_messages = [
|
||||||
|
f"本群通过祈愿召唤了勇者大人:[CQ:at,qq={user_id}],欢迎加入!",
|
||||||
|
f"欢迎 [CQ:at,qq={user_id}] 加入本群!请发送帮助查看更多功能~",
|
||||||
|
f"[CQ:at,qq={user_id}] 已成功加入蛋定助手大家庭!请发送帮助查看更多功能,祝您使用愉快~"
|
||||||
|
]
|
||||||
|
# 随机选择一条欢迎语
|
||||||
|
welcome_text = random.choice(welcome_messages)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取帮助菜单图片的绝对路径
|
||||||
|
# 这里不需要获取父目录,直接引用danding_help插件的路径
|
||||||
|
image_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||||
|
"danding_help", "img", "帮助菜单.jpg")
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
logger.error(f"帮助菜单图片不存在: {image_path}")
|
||||||
|
await group_welcome.finish(Message(welcome_text))
|
||||||
|
return
|
||||||
|
|
||||||
|
# 读取图片
|
||||||
|
with open(image_path, "rb") as f:
|
||||||
|
image_bytes = f.read()
|
||||||
|
|
||||||
|
# 添加随机延迟,模拟人工反应
|
||||||
|
await asyncio.sleep(random.uniform(2, 3))
|
||||||
|
|
||||||
|
# 发送欢迎消息和帮助菜单图片
|
||||||
|
await MessageFactory([
|
||||||
|
Text(welcome_text),
|
||||||
|
Image(image_bytes)
|
||||||
|
]).send()
|
||||||
|
|
||||||
|
logger.info(f"已发送欢迎消息给新成员 {user_id} 在群 {event.group_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送欢迎消息失败: {e}")
|
||||||
|
# 发生错误时尝试直接发送文本消息
|
||||||
|
await group_welcome.finish(Message(welcome_text))
|
||||||
BIN
data/chatai/output.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
data/chouka/n/唐纸伞妖.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
data/chouka/n/天邪鬼绿.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
data/chouka/n/天邪鬼赤.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
data/chouka/n/天邪鬼青.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
data/chouka/n/天邪鬼黄.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
data/chouka/n/寄生魂.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
data/chouka/n/帚神.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
data/chouka/n/提灯小僧.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
data/chouka/n/涂壁.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
data/chouka/n/灯笼鬼.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
data/chouka/n/盗墓小鬼.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
data/chouka/n/赤舌.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
data/chouka/r/三尾狐.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
data/chouka/r/丑时之女.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
data/chouka/r/九命猫.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
data/chouka/r/兵俑.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
data/chouka/r/巫蛊师.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
data/chouka/r/座敷童子.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
data/chouka/r/武士之灵.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
data/chouka/r/河童.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
data/chouka/r/狸猫.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
data/chouka/r/童女.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
data/chouka/r/童男.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
data/chouka/r/跳跳妹妹.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
data/chouka/r/跳跳弟弟.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
data/chouka/r/雨女.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
data/chouka/r/食发鬼.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
data/chouka/r/饿鬼.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
data/chouka/r/鲤鱼精.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
data/chouka/r/鸦天狗.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
data/chouka/sp/初翎山风.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
data/chouka/sp/夜溟彼岸花.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
data/chouka/sp/天剑韧心鬼切.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
data/chouka/sp/少羽大天狗.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
data/chouka/sp/待宵姑获鸟.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
data/chouka/sp/御怨般若.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
data/chouka/sp/浮世青行灯.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
data/chouka/sp/炼狱茨木童子.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
data/chouka/sp/烬天玉藻前.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
data/chouka/sp/稻荷神御馔津.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
data/chouka/sp/缚骨清姬.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
data/chouka/sp/聆海金鱼姬.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
data/chouka/sp/苍风一目连.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
data/chouka/sp/蝉冰雪女.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
data/chouka/sp/赤影妖刀姬.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
data/chouka/sp/骁浪荒川之主.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
data/chouka/sp/鬼王酒吞童子.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
data/chouka/sp/麓铭大岳丸.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
data/chouka/sr/傀儡师.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
data/chouka/sr/凤凰火.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
data/chouka/sr/判官.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |