首次提交
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 |