首次提交

This commit is contained in:
2025-12-26 22:41:42 +08:00
commit 4a944316fe
143 changed files with 17550 additions and 0 deletions

1
.env.dev Normal file
View File

@@ -0,0 +1 @@
LOG_LEVEL=DEBUG

0
.env.prod Normal file
View File

141
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
# 备份文件
rm NoneBot_DanDing_3.10.12.zip
zip -r NoneBot_DanDing_3.10.12.zip *

View 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="""
# 自动好友请求接受插件
当收到好友请求时,会自动同意
无需用户操作,插件自动处理所有好友请求
""",
)

View 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}")

View 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] = ""

View 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)}")

View File

@@ -0,0 +1,4 @@
from pydantic import BaseModel, Field
class Config(BaseModel):
recall_delay: int = Field(default=110, env="RECALL_DELAY") # 撤回延迟时间,默认 110 秒

View 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

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

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

View 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 # 重新抛出异常以便上层处理

View File

@@ -0,0 +1 @@
"""文本处理工具包"""

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

View File

@@ -0,0 +1,4 @@
from . import command_list
from .config import Config
__plugin_meta__ = Config

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

View 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"

View 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("登录失败,请检查账号密码或验证码是否正确")

View 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="""
指令:
- 大漠余额
- 余额查询
权限:
仅限指定用户QQ1424473282使用
使用流程:
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("获取失败、登录失败,请检查账号密码或验证码是否正确")

View File

@@ -0,0 +1,5 @@
from pydantic import BaseModel
class Config(BaseModel):
"""Plugin Config Here"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

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

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

View 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"

View 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

View File

@@ -0,0 +1,15 @@
# 欢迎使用蛋定助手🎇
## 发送以下关键词获取帮助🤝🏻
1. 下载dl
2. 帮助文档wd
3. 公益版gyb
4. 正式版zsb
5. 下单xd
## 部分教程关键字✨️
1. 正式版御魂双开dyh
2. 正式版如何运行htr
## 活动🔥🔥
1. 每日试用mrss - 永久活动单设备每日可试用1小时

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

View 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. 蛋定の卡铺1https://shop.danding.vip
2. 蛋定の卡铺2https://ka.x-tools.top
"""
DailyTrialStr:str = """\
永久活动-每日试用单设备支持每日试用1小时的脚本时长
请在购买或支持蛋定助手前,一定要先试用,确保自己可以正常使用脚本!"""

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

View 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}")

View 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

View 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"

View 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

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

View 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">阴阳师抽卡系统 - 管理后台 &copy; 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>

View 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}"

View 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

View 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)时,会自动欢迎并发送帮助菜单
注意:本插件仅在特定群组中可用
""",
)

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
data/chouka/n/寄生魂.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
data/chouka/n/帚神.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
data/chouka/n/涂壁.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
data/chouka/n/灯笼鬼.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
data/chouka/n/赤舌.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
data/chouka/r/三尾狐.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
data/chouka/r/九命猫.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
data/chouka/r/兵俑.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
data/chouka/r/巫蛊师.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
data/chouka/r/河童.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
data/chouka/r/狸猫.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
data/chouka/r/童女.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
data/chouka/r/童男.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
data/chouka/r/雨女.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
data/chouka/r/食发鬼.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
data/chouka/r/饿鬼.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
data/chouka/r/鲤鱼精.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
data/chouka/r/鸦天狗.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
data/chouka/sr/判官.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Some files were not shown because too many files have changed in this diff Show More