Compare commits
78 Commits
task/wps-b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a70a53383d | |||
| 79924a713d | |||
| ed6406cfc9 | |||
| 3d89dbf8f4 | |||
| 97692b120a | |||
| c7a4d3d047 | |||
| 58e6ee851c | |||
| aef45eb9a4 | |||
| 8eeec67730 | |||
| 6c2227debe | |||
|
|
7c4d0a0ef2 | ||
| 4027771a65 | |||
|
|
9ed8fffcf7 | ||
| bcf93e910a | |||
| 8487e6e931 | |||
| aaff6cee86 | |||
| 6e63b361e4 | |||
| d88edc31fc | |||
| ff709eadca | |||
| 156a0e5752 | |||
| e99d8f4914 | |||
| c1e3082f08 | |||
| 3f3c21c3d6 | |||
| b07b044035 | |||
| 9e16774d2f | |||
| 17da2e6111 | |||
| 1f10799562 | |||
| cef684f64b | |||
| 7962852685 | |||
| cc97374e98 | |||
| a62d5d66b7 | |||
| 19cde88acf | |||
| 6f05ca98f1 | |||
| 5a8a4ac026 | |||
| 12aac846cc | |||
| 4bddd4339f | |||
| 927c16e1fc | |||
| 8ffd261bb0 | |||
| 3d4f754a0a | |||
| 0a60a7fc4b | |||
| 82dc616495 | |||
| e26936acee | |||
| 643c516baf | |||
| e2e039b9b1 | |||
| a76de359d5 | |||
| b247c57bbe | |||
| a893e54166 | |||
| 8eb9f26cfd | |||
| 01248b9092 | |||
| 9504c57aaf | |||
| 5742adc2ad | |||
| 9718eb0614 | |||
| 27aee22f62 | |||
| 37fccb3021 | |||
| 218e1f1045 | |||
| b57f1e08ef | |||
| 581a516610 | |||
| 8e883fe5e1 | |||
| 5e2afca960 | |||
| 0c2638e948 | |||
| 7b72ed9f34 | |||
| 08cc4870ca | |||
| c80fb67165 | |||
| d36bb2de83 | |||
| 38e81dbfe6 | |||
| 003ff9a94e | |||
| 7483a00a99 | |||
| c05a8b3578 | |||
| 1439523253 | |||
|
|
278e760fb2 | ||
| 57f955f837 | |||
| 7d28e2e2aa | |||
| f217cd958b | |||
| 93a4882da2 | |||
| 38cd441908 | |||
| b7a57539f5 | |||
| c4be929b3a | |||
| c761a12377 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -181,3 +181,7 @@ cython_debug/
|
||||
.cursorindexingignore
|
||||
# IDE
|
||||
.vscode/
|
||||
|
||||
# Database
|
||||
data/bot.db
|
||||
liubai_web.pid
|
||||
238
.tasks/2025-10-28_1_add-idiom-chain-game.md
Normal file
238
.tasks/2025-10-28_1_add-idiom-chain-game.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# 背景
|
||||
文件名:2025-10-28_1_add-idiom-chain-game.md
|
||||
创建于:2025-10-28_15:43:00
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/add-idiom-chain-game_2025-10-28_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在WPS Bot Game项目中新增一个成语接龙游戏功能。
|
||||
|
||||
## 核心需求
|
||||
1. 群内多人游戏,机器人作为裁判和出题者
|
||||
2. 允许按拼音接龙(包括谐音接龙)
|
||||
3. 没有时间限制
|
||||
4. 不需要提示功能
|
||||
5. 游戏记录保存到.stats统计中
|
||||
6. 不允许重复使用成语
|
||||
7. 不需要难度分级(非人机对战)
|
||||
8. 需要裁判指令用于接受/拒绝玩家回答
|
||||
|
||||
## 游戏玩法
|
||||
- 机器人出题(给出起始成语)
|
||||
- 群内玩家轮流接龙
|
||||
- 机器人判断接龙是否有效(拼音/谐音匹配、未重复使用)
|
||||
- 裁判可以手动接受或拒绝某个回答
|
||||
- 记录每个玩家的成功接龙次数
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/
|
||||
│ ├── database.py # SQLite数据库操作
|
||||
│ ├── middleware.py # 中间件
|
||||
│ └── models.py # 数据模型
|
||||
├── games/ # 游戏模块
|
||||
│ ├── base.py # 游戏基类
|
||||
│ ├── dice.py # 骰娘游戏
|
||||
│ ├── rps.py # 石头剪刀布
|
||||
│ ├── fortune.py # 运势占卜
|
||||
│ ├── guess.py # 猜数字
|
||||
│ └── quiz.py # 问答游戏
|
||||
├── data/ # 数据文件
|
||||
│ ├── bot.db # SQLite数据库
|
||||
│ ├── quiz.json # 问答题库
|
||||
│ └── fortunes.json # 运势数据
|
||||
├── routers/ # 路由处理
|
||||
│ ├── callback.py # WPS回调处理
|
||||
│ └── health.py # 健康检查
|
||||
└── utils/ # 工具模块
|
||||
├── message.py # 消息发送
|
||||
├── parser.py # 指令解析
|
||||
└── rate_limit.py # 限流控制
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
- FastAPI:Web框架
|
||||
- SQLite:数据存储
|
||||
- WPS协作机器人API:消息接收与发送
|
||||
|
||||
## 现有游戏架构
|
||||
1. 所有游戏继承`BaseGame`基类
|
||||
2. 必须实现`handle(command, chat_id, user_id)`方法处理指令
|
||||
3. 必须实现`get_help()`方法返回帮助信息
|
||||
4. 游戏状态存储在数据库`game_states`表:`(chat_id, user_id, game_type)`作为联合主键
|
||||
5. 游戏统计存储在`game_stats`表:记录`wins`, `losses`, `draws`, `total_plays`
|
||||
6. 指令通过`CommandParser`解析,在`callback.py`中分发到对应游戏处理器
|
||||
|
||||
## 数据库设计
|
||||
### game_states表
|
||||
- chat_id: 会话ID
|
||||
- user_id: 用户ID
|
||||
- game_type: 游戏类型
|
||||
- state_data: JSON格式的游戏状态数据
|
||||
- created_at/updated_at: 时间戳
|
||||
|
||||
### game_stats表
|
||||
- user_id: 用户ID
|
||||
- game_type: 游戏类型
|
||||
- wins/losses/draws/total_plays: 统计数据
|
||||
|
||||
# 分析
|
||||
|
||||
## 关键技术挑战
|
||||
|
||||
### 1. 群级别vs个人级别状态管理
|
||||
现有游戏(猜数字、问答)都是个人独立状态,使用`(chat_id, user_id, game_type)`作为主键。
|
||||
|
||||
成语接龙是群内共享游戏,需要:
|
||||
- 群级别的游戏状态:当前成语、已用成语列表、接龙长度、当前轮到谁
|
||||
- 个人级别的统计:每个玩家的成功接龙次数
|
||||
|
||||
**可能方案:**
|
||||
- 使用特殊user_id(如0或-1)存储群级别游戏状态
|
||||
- 或者在state_data中存储所有玩家信息
|
||||
|
||||
### 2. 成语词库准备
|
||||
需要准备:
|
||||
- 成语列表(至少500-1000个常用成语)
|
||||
- 每个成语的拼音信息(用于判断接龙是否匹配)
|
||||
- 数据格式:JSON文件,类似quiz.json
|
||||
|
||||
### 3. 拼音匹配逻辑
|
||||
- 需要拼音库支持(pypinyin)
|
||||
- 支持谐音匹配(声母韵母匹配)
|
||||
- 处理多音字情况
|
||||
|
||||
### 4. 裁判指令设计
|
||||
需要额外指令:
|
||||
- `.idiom accept` - 接受上一个回答
|
||||
- `.idiom reject` - 拒绝上一个回答
|
||||
- 需要权限控制(谁可以当裁判?)
|
||||
|
||||
### 5. 游戏流程设计
|
||||
```
|
||||
1. 开始游戏:.idiom start
|
||||
- 机器人随机给出起始成语
|
||||
- 创建群级别游戏状态
|
||||
|
||||
2. 玩家接龙:.idiom [成语]
|
||||
- 检查是否在词库中
|
||||
- 检查拼音是否匹配(首字拼音 == 上一个成语尾字拼音)
|
||||
- 检查是否已使用过
|
||||
- 自动判断或等待裁判确认
|
||||
|
||||
3. 裁判操作:.idiom accept/reject
|
||||
- 手动接受或拒绝最近的回答
|
||||
|
||||
4. 查看状态:.idiom status
|
||||
- 显示当前成语、已用成语数量、参与人数
|
||||
|
||||
5. 结束游戏:.idiom stop
|
||||
- 显示统计信息
|
||||
- 更新每个玩家的game_stats
|
||||
```
|
||||
|
||||
## 现有代码分析
|
||||
|
||||
### CommandParser (utils/parser.py)
|
||||
需要添加成语接龙指令映射:
|
||||
```python
|
||||
'.idiom': 'idiom',
|
||||
'.成语接龙': 'idiom',
|
||||
```
|
||||
|
||||
### callback.py (routers/callback.py)
|
||||
需要在`handle_command`函数中添加idiom游戏分支:
|
||||
```python
|
||||
if game_type == 'idiom':
|
||||
from games.idiom import IdiomGame
|
||||
game = IdiomGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
### base.py (games/base.py)
|
||||
需要更新`get_help_message()`和`get_stats_message()`,添加成语接龙信息。
|
||||
|
||||
### config.py
|
||||
可能需要添加成语接龙相关配置:
|
||||
```python
|
||||
"idiom": {
|
||||
"auto_judge": True, # 是否自动判断
|
||||
"require_approval": False, # 是否需要裁判确认
|
||||
}
|
||||
```
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
(待INNOVATE模式填写)
|
||||
|
||||
# 当前执行步骤:"1. 创建任务文件"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## [2025-10-28 15:45:00]
|
||||
- 已修改:requirements.txt, config.py, utils/parser.py, routers/callback.py, games/base.py
|
||||
- 已创建:games/idiom.py
|
||||
- 更改:
|
||||
1. 在requirements.txt中添加pypinyin==0.51.0依赖
|
||||
2. 在config.py的GAME_CONFIG中添加idiom配置(起始成语池、历史显示数量)
|
||||
3. 在utils/parser.py的COMMAND_MAP中添加.idiom、.成语接龙、.成语指令映射
|
||||
4. 创建games/idiom.py实现完整的成语接龙游戏逻辑:
|
||||
- IdiomGame类继承BaseGame
|
||||
- 实现handle()主指令分发方法
|
||||
- 实现_start_game()开始游戏
|
||||
- 实现_make_chain()玩家接龙
|
||||
- 实现_set_next_user()指定下一位
|
||||
- 实现_reject_idiom()裁判拒绝
|
||||
- 实现_show_status()显示状态
|
||||
- 实现_show_blacklist()显示黑名单
|
||||
- 实现_stop_game()结束游戏
|
||||
- 实现_get_pinyin()获取拼音(支持多音字)
|
||||
- 实现_check_pinyin_match()检查拼音匹配(忽略声调)
|
||||
- 实现_parse_mentioned_user()解析@用户
|
||||
- 实现_can_answer()权限检查(防连续、指定轮次)
|
||||
- 实现_validate_idiom()词语验证(4字、拼音匹配、未使用、未黑名单)
|
||||
- 实现get_help()帮助信息
|
||||
5. 在routers/callback.py的handle_command()中添加idiom游戏分支
|
||||
6. 在games/base.py的get_help_message()中添加成语接龙帮助信息
|
||||
7. 在games/base.py的get_stats_message()的game_names字典中添加idiom映射
|
||||
- 原因:实现成语接龙游戏功能的所有核心代码
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
## [2025-10-28 15:50:00]
|
||||
- 已修改:games/base.py
|
||||
- 更改:在get_help_message()的成语接龙部分添加黑名单相关指令说明
|
||||
- 添加 `.idiom reject [词语]` - 拒绝词语加入黑名单(仅发起人)
|
||||
- 添加 `.idiom blacklist` - 查看黑名单
|
||||
- 原因:用户反馈.help帮助信息中看不到黑名单机制的使用说明
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
## [2025-10-28 15:55:00]
|
||||
- 已修改:games/idiom.py
|
||||
- 已创建:data/idiom_blacklist.json
|
||||
- 更改:将黑名单机制从游戏状态改为全局永久存储
|
||||
1. 创建data/idiom_blacklist.json作为全局黑名单数据文件
|
||||
2. 在IdiomGame.__init__()中添加黑名单文件路径和懒加载变量
|
||||
3. 添加_load_blacklist()方法从文件懒加载全局黑名单
|
||||
4. 添加_save_blacklist()方法保存黑名单到文件
|
||||
5. 修改_validate_idiom()方法检查全局黑名单而非游戏状态中的黑名单
|
||||
6. 修改_start_game()方法移除state_data中的blacklist字段初始化
|
||||
7. 修改_reject_idiom()方法将词语添加到全局黑名单并保存到文件
|
||||
8. 修改_show_blacklist()方法显示全局黑名单,不再依赖游戏状态
|
||||
9. 更新所有提示信息,明确说明是"永久禁用"
|
||||
- 原因:用户要求被拒绝的词语应该永久不可用,而不是仅本局游戏不可用
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
# 最终审查
|
||||
|
||||
(待REVIEW模式完成后填写)
|
||||
|
||||
271
.tasks/2025-10-28_1_gomoku.md
Normal file
271
.tasks/2025-10-28_1_gomoku.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# 背景
|
||||
文件名:2025-10-28_1_gomoku.md
|
||||
创建于:2025-10-28_17:08:29
|
||||
创建者:User
|
||||
主分支:main
|
||||
任务分支:task/gomoku_2025-10-28_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
创建一个五子棋(Gomoku)游戏模块,支持双人对战功能。
|
||||
|
||||
## 核心需求
|
||||
1. **游戏模式**:双人对战(两个用户在同一个聊天中对战)
|
||||
2. **棋盘规格**:标准15x15棋盘
|
||||
3. **禁手规则**:需要实现禁手规则(三三禁手、四四禁手、长连禁手)
|
||||
4. **超时规则**:不需要回合时间限制
|
||||
5. **并发对战**:允许多轮对战同时存在,只要交战双方不同即可
|
||||
6. **显示方式**:使用emoji绘制棋盘(⚫⚪➕)+ 坐标系统(A-O列,1-15行)
|
||||
|
||||
## 功能清单
|
||||
- 开始游戏:`.gomoku start @对手` 或 `.gomoku @对手`
|
||||
- 落子:`.gomoku A1` 或 `.gomoku 落子 A1`
|
||||
- 认输:`.gomoku resign` 或 `.gomoku 认输`
|
||||
- 查看棋盘:`.gomoku show` 或 `.gomoku 查看`
|
||||
- 查看战绩:`.gomoku stats` 或 `.gomoku 战绩`
|
||||
- 帮助信息:`.gomoku help` 或 `.gomoku 帮助`
|
||||
|
||||
## 技术要点
|
||||
1. 继承`BaseGame`基类
|
||||
2. 游戏状态存储在数据库中(使用chat_id + 对战双方ID作为键)
|
||||
3. 需要实现五子棋禁手规则的判定逻辑
|
||||
4. 需要实现胜负判定(五子连珠)
|
||||
5. 棋盘使用二维数组表示,支持坐标转换(A-O, 1-15)
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 现有架构
|
||||
- **框架**:FastAPI
|
||||
- **数据库**:SQLite(使用标准库sqlite3)
|
||||
- **游戏基类**:`games/base.py - BaseGame`
|
||||
- **路由处理**:`routers/callback.py`
|
||||
- **数据库操作**:`core/database.py - Database类`
|
||||
|
||||
## 现有游戏
|
||||
- 石头剪刀布(rps)
|
||||
- 问答游戏(quiz)
|
||||
- 猜数字(guess)
|
||||
- 成语接龙(idiom)
|
||||
- 骰娘系统(dice)
|
||||
- 运势占卜(fortune)
|
||||
|
||||
## 数据库表结构
|
||||
1. **users**:用户基本信息
|
||||
2. **game_states**:游戏状态(支持chat_id, user_id, game_type的唯一约束)
|
||||
3. **game_stats**:游戏统计(wins, losses, draws, total_plays)
|
||||
|
||||
# 分析
|
||||
|
||||
## 核心挑战
|
||||
|
||||
### 1. 游戏状态管理
|
||||
- 现有的`game_states`表使用`(chat_id, user_id, game_type)`作为唯一键
|
||||
- 五子棋需要双人对战,需要同时记录两个玩家
|
||||
- 需要设计状态数据结构,存储:
|
||||
- 对战双方ID(player1_id, player2_id)
|
||||
- 当前轮到谁(current_player_id)
|
||||
- 棋盘状态(15x15二维数组)
|
||||
- 游戏状态(waiting, playing, finished)
|
||||
- 胜者ID(winner_id,如果有)
|
||||
|
||||
### 2. 多轮对战并发
|
||||
- 允许同一个chat中有多轮对战,只要对战双方不同
|
||||
- 需要一个机制来标识不同的对战局(可以用对战双方ID的组合)
|
||||
- 状态查询需要能够找到特定用户参与的对战
|
||||
|
||||
### 3. 禁手规则实现
|
||||
禁手规则(仅对黑方,即先手玩家):
|
||||
- **三三禁手**:一手棋同时形成两个或以上的活三
|
||||
- **四四禁手**:一手棋同时形成两个或以上的活四或冲四
|
||||
- **长连禁手**:一手棋形成六子或以上的连珠
|
||||
|
||||
需要实现:
|
||||
- 判断某个位置的四个方向(横、竖、左斜、右斜)的连珠情况
|
||||
- 判断活三、活四、冲四的定义
|
||||
- 在落子时检查是否触发禁手
|
||||
|
||||
### 4. 坐标系统
|
||||
- 列:A-O(15列)
|
||||
- 行:1-15(15行)
|
||||
- 需要坐标转换函数:`parse_coord("A1") -> (0, 0)`
|
||||
- 需要显示转换函数:`format_coord(0, 0) -> "A1"`
|
||||
|
||||
### 5. 棋盘显示
|
||||
使用emoji:
|
||||
- ⚫ 黑子(先手)
|
||||
- ⚪ 白子(后手)
|
||||
- ➕ 空位
|
||||
- 需要添加行号和列号标注
|
||||
|
||||
示例:
|
||||
```
|
||||
A B C D E F G H I J K L M N O
|
||||
1 ➕➕➕➕➕➕➕➕➕➕➕➕➕➕➕
|
||||
2 ➕➕➕➕➕➕➕➕➕➕➕➕➕➕➕
|
||||
3 ➕➕⚫➕➕➕➕➕➕➕➕➕➕➕➕
|
||||
...
|
||||
```
|
||||
|
||||
## 数据结构设计
|
||||
|
||||
### state_data结构
|
||||
```python
|
||||
{
|
||||
"player1_id": 123456, # 黑方(先手)
|
||||
"player2_id": 789012, # 白方(后手)
|
||||
"current_player": 123456, # 当前轮到谁
|
||||
"board": [[0]*15 for _ in range(15)], # 0:空, 1:黑, 2:白
|
||||
"status": "playing", # waiting, playing, finished
|
||||
"winner_id": None, # 胜者ID
|
||||
"moves": [], # 历史落子记录 [(row, col, player_id), ...]
|
||||
"last_move": None # 最后一手 (row, col)
|
||||
}
|
||||
```
|
||||
|
||||
### 游戏状态存储策略
|
||||
- 使用chat_id作为会话ID
|
||||
- 使用较小的user_id作为主键中的user_id(保证唯一性)
|
||||
- 在state_data中存储完整的对战信息
|
||||
- 查询时需要检查用户是否是player1或player2
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案选择
|
||||
使用现有的数据库表结构,通过精心设计state_data来支持双人对战。
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. 游戏类:`games/gomoku.py`
|
||||
继承`BaseGame`,实现以下方法:
|
||||
- `handle()` - 主处理逻辑
|
||||
- `get_help()` - 帮助信息
|
||||
- `_start_game()` - 开始游戏
|
||||
- `_make_move()` - 落子
|
||||
- `_show_board()` - 显示棋盘
|
||||
- `_resign()` - 认输
|
||||
- `_get_stats()` - 查看战绩
|
||||
|
||||
### 2. 五子棋逻辑:单独模块或工具类
|
||||
- `_parse_coord()` - 解析坐标
|
||||
- `_format_coord()` - 格式化坐标
|
||||
- `_render_board()` - 渲染棋盘
|
||||
- `_check_win()` - 检查胜负
|
||||
- `_check_forbidden()` - 检查禁手
|
||||
- `_is_valid_move()` - 检查落子是否合法
|
||||
|
||||
### 3. 禁手检测逻辑
|
||||
实现辅助方法:
|
||||
- `_count_line()` - 统计某方向的连珠情况
|
||||
- `_is_live_three()` - 判断活三
|
||||
- `_is_live_four()` - 判断活四
|
||||
- `_is_rush_four()` - 判断冲四
|
||||
- `_check_three_three()` - 检查三三禁手
|
||||
- `_check_four_four()` - 检查四四禁手
|
||||
- `_check_overline()` - 检查长连禁手
|
||||
|
||||
### 4. 状态管理
|
||||
- 使用`min(player1_id, player2_id)`作为数据库中的user_id
|
||||
- 在state_data中完整存储对战信息
|
||||
- 提供辅助方法查找用户当前参与的游戏
|
||||
|
||||
### 5. 路由注册
|
||||
在`routers/callback.py`的`handle_command()`函数中添加:
|
||||
```python
|
||||
if game_type == 'gomoku':
|
||||
from games.gomoku import GomokuGame
|
||||
game = GomokuGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
### 6. 指令解析
|
||||
在`utils/parser.py`的`CommandParser`类中添加gomoku指令识别
|
||||
|
||||
### 7. 配置更新
|
||||
在`config.py`中添加五子棋相关配置(如果需要)
|
||||
|
||||
# 当前执行步骤:"已完成所有实施步骤"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## [2025-10-28 17:18:21]
|
||||
- 已修改:
|
||||
- config.py - 添加gomoku配置
|
||||
- utils/parser.py - 添加gomoku指令映射
|
||||
- games/gomoku_logic.py - 创建五子棋逻辑模块(新文件)
|
||||
- games/gomoku.py - 创建五子棋游戏类(新文件)
|
||||
- routers/callback.py - 添加gomoku路由
|
||||
- games/base.py - 更新帮助信息和统计信息
|
||||
- 更改:完成五子棋游戏的完整实现,包括:
|
||||
- 群级游戏池管理(支持多轮对战并存)
|
||||
- 标准15x15棋盘
|
||||
- 完整的禁手规则(三三、四四、长连)
|
||||
- 坐标系统(A-O列,1-15行)
|
||||
- emoji棋盘渲染(⚫⚪➕)
|
||||
- 胜负判定
|
||||
- 战绩统计
|
||||
- 原因:实现用户需求的双人对战五子棋游戏
|
||||
- 阻碍因素:用户识别和显示格式错误
|
||||
- 状态:不成功
|
||||
|
||||
## [2025-10-28 17:36:07]
|
||||
- 已修改:
|
||||
- games/gomoku.py - 修复用户识别和显示格式
|
||||
- 更改:
|
||||
- 修复 `_parse_opponent()` 方法,使用正确的WPS @用户格式 `<at user_id="xxx"></at>` 进行解析
|
||||
- 修改所有用户显示,从 `@用户{user_id}` 改为 `<at user_id="{user_id}"></at>`,以正确显示用户的群名称
|
||||
- 涉及修改:开始游戏、落子、显示棋盘、认输、列出对战等所有用户显示位置
|
||||
- 原因:修复用户识别失败和显示错误的问题
|
||||
- 阻碍因素:用户识别仍然失败
|
||||
- 状态:不成功
|
||||
|
||||
## [2025-10-28 17:42:24]
|
||||
- 已修改:
|
||||
- games/gomoku.py - 改进用户ID解析和添加调试日志
|
||||
- routers/callback.py - 增强日志输出
|
||||
- 更改:
|
||||
- 改进 `_parse_opponent()` 方法,支持多种@用户格式(双引号、单引号、不同的标签格式)
|
||||
- 在 `handle()` 方法中添加详细的调试日志(command, args, action, opponent_id)
|
||||
- 改进错误提示,显示实际接收到的参数内容
|
||||
- 将 callback.py 中的消息内容日志级别从 DEBUG 改为 INFO,便于追踪
|
||||
- 原因:进一步诊断用户ID识别失败的问题,添加调试信息帮助定位问题
|
||||
- 阻碍因素:WPS callback不提供被@用户的ID信息
|
||||
- 状态:不成功
|
||||
|
||||
## [2025-10-28 17:55:00]
|
||||
- 已修改:
|
||||
- games/gomoku.py - 重构游戏发起机制,从@用户改为挑战-接受模式
|
||||
- games/base.py - 更新全局帮助信息
|
||||
- routers/callback.py - 添加完整callback数据日志
|
||||
- 更改:
|
||||
- **核心架构变更**:从"@用户发起对战"改为"挑战-接受"机制
|
||||
- 新增 `_create_challenge()` 方法 - 用户发起挑战
|
||||
- 新增 `_accept_challenge()` 方法 - 其他用户接受挑战
|
||||
- 新增 `_cancel_challenge()` 方法 - 取消自己的挑战
|
||||
- 删除 `_parse_opponent()` 方法(不再需要)
|
||||
- 删除 `_start_game()` 方法(由新方法替代)
|
||||
- 更新游戏池数据结构,添加 `challenges` 列表
|
||||
- 更新所有帮助信息和错误提示
|
||||
- 指令变更:
|
||||
- `.gomoku challenge` / `.gomoku start` - 发起挑战
|
||||
- `.gomoku accept` / `.gomoku join` - 接受挑战
|
||||
- `.gomoku cancel` - 取消挑战
|
||||
- 原因:WPS callback消息内容中@用户只是文本形式(如"@揭英飙"),不包含user_id,无法实现@用户发起对战
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## [2025-10-28 17:56:03]
|
||||
- 已修改:
|
||||
- games/gomoku_logic.py - 修复棋盘对齐问题
|
||||
- 更改:
|
||||
- 优化 `render_board()` 函数的格式化逻辑
|
||||
- 列标题:每个字母后面加一个空格,确保与棋子列对齐
|
||||
- 行号:调整前导空格,从 " {row_num} " 改为 "{row_num} "
|
||||
- 棋子:每个emoji后面加一个空格,行尾去除多余空格
|
||||
- 整体对齐:确保列标题、行号、棋子三者在Markdown代码块中正确对齐
|
||||
- 原因:修复用户反馈的棋盘文本对齐问题
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
# 最终审查
|
||||
(待完成后填写)
|
||||
|
||||
218
.tasks/2025-10-29_1_add-user-register.md
Normal file
218
.tasks/2025-10-29_1_add-user-register.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# 背景
|
||||
文件名:2025-10-29_1_add-user-register.md
|
||||
创建于:2025-10-29_15:42:51
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:main
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在WPS Bot Game项目中添加用户注册系统,让用户可以通过 `.register name` 命令将用户ID与名称绑定。
|
||||
|
||||
## 核心需求
|
||||
1. 用户可以通过 `.register name` 命令注册或更新自己的名称
|
||||
2. 这个名称是全局的,所有游戏和功能都可以使用
|
||||
3. 积分赠送功能需要支持通过用户名查找用户,而不仅仅是用户ID
|
||||
4. 需要一个全局的用户名称解析机制
|
||||
|
||||
## 问题背景
|
||||
目前消息传递中的用户ID和用户名不一致,使用起来非常困难。比如赠送积分时指定用户ID太麻烦了。添加注册系统后,可以使用 `.gift username points` 而不是 `.gift 123456 points`。
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/
|
||||
│ ├── database.py # SQLite数据库操作
|
||||
│ ├── middleware.py # 中间件
|
||||
│ └── models.py # 数据模型
|
||||
├── routers/
|
||||
│ ├── callback.py # Callback路由处理
|
||||
│ └── health.py # 健康检查
|
||||
├── games/ # 游戏模块
|
||||
│ ├── base.py # 游戏基类
|
||||
│ ├── gift.py # 积分赠送系统
|
||||
│ └── ... # 其他游戏
|
||||
└── utils/
|
||||
├── parser.py # 指令解析
|
||||
└── message.py # 消息发送
|
||||
```
|
||||
|
||||
# 分析
|
||||
|
||||
## 当前状态
|
||||
1. `users` 表已经有 `username` 字段,但没有被充分利用
|
||||
2. `get_or_create_user()` 可以接收 username 参数,但实际调用时没有传
|
||||
3. `gift.py` 目前只能通过用户ID进行赠送
|
||||
4. 缺少通过用户名查找用户的功能
|
||||
|
||||
## 关键技术点
|
||||
1. **数据库层**: 需要添加根据 username 查找用户的方
|
||||
2. 需要添加更新用户名的功能
|
||||
3. **指令解析层**: 需要添加 `.register` 指令的识别
|
||||
4. **路由层**: 需要添加 register 类型的处理逻辑
|
||||
5. **应用层**: 需要实现注册逻辑,并修改 gift 功能支持用户名
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. 在 `database.py` 中添加两个方法:
|
||||
- `get_user_by_name(username: str)` - 根据用户名查找用户
|
||||
- `update_user_name(user_id: int, username: str)` - 更新用户名称
|
||||
2. 在 `parser.py` 中添加 `.register` 指令映射
|
||||
3. 在 `callback.py` 中添加 register 类型的处理逻辑
|
||||
4. 修改 `gift.py` 支持通过用户名赠送积分
|
||||
5. 创建注册处理逻辑(可以单独文件或集成到 callback)
|
||||
|
||||
## 设计决策
|
||||
- 用户名作为额外的查找方式,但不替代 user_id(保持数据库主键稳定)
|
||||
- 用户名不强制唯一(允许相同昵称)
|
||||
- 注册功能独立于游戏模块,放在顶层处理
|
||||
|
||||
# 当前执行步骤:"3. 等待用户确认"
|
||||
|
||||
# 详细实施计划
|
||||
|
||||
## 文件1: core/database.py
|
||||
|
||||
### 添加方法1: get_user_by_name()
|
||||
```python
|
||||
def get_user_by_name(self, username: str) -> Optional[Dict]:
|
||||
"""根据用户名查找用户
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
|
||||
Returns:
|
||||
用户信息字典,如果不存在返回None
|
||||
"""
|
||||
```
|
||||
|
||||
### 添加方法2: update_user_name()
|
||||
```python
|
||||
def update_user_name(self, user_id: int, username: str) -> bool:
|
||||
"""更新用户名称
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
username: 新用户名
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
```
|
||||
|
||||
### 添加数据库索引
|
||||
在 `init_tables()` 方法中添加:
|
||||
```sql
|
||||
CREATE INDEX IF NOT EXISTS idx_username ON users(username)
|
||||
```
|
||||
|
||||
## 文件2: utils/parser.py
|
||||
|
||||
### 修改 COMMAND_MAP
|
||||
在现有的 COMMAND_MAP 中添加:
|
||||
```python
|
||||
'.register': 'register',
|
||||
'.注册': 'register',
|
||||
```
|
||||
|
||||
## 文件3: routers/callback.py
|
||||
|
||||
### 在 handle_command() 中添加处理
|
||||
在现有游戏类型判断后添加:
|
||||
```python
|
||||
# 注册系统
|
||||
if game_type == 'register':
|
||||
return await handle_register_command(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
### 添加处理函数
|
||||
```python
|
||||
async def handle_register_command(command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理注册命令
|
||||
|
||||
Args:
|
||||
command: 完整指令 ".register name"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
注册结果消息
|
||||
"""
|
||||
```
|
||||
|
||||
## 文件4: games/gift.py
|
||||
|
||||
### 修改 _process_gift_command()
|
||||
修改参数解析逻辑,支持用户名和用户ID两种格式:
|
||||
```python
|
||||
def _process_gift_command(self, args: str, sender_id: int) -> str:
|
||||
# 尝试解析为用户名或用户ID
|
||||
parts = args.split(maxsplit=2)
|
||||
|
||||
if len(parts) < 2:
|
||||
return "❌ 指令格式错误..."
|
||||
|
||||
# 检查第一部分是用户名还是ID
|
||||
receiver_input = parts[0]
|
||||
|
||||
if receiver_input.isdigit():
|
||||
# 是数字,作为用户ID处理
|
||||
receiver_id = int(receiver_input)
|
||||
else:
|
||||
# 是用户名,通过数据库查找
|
||||
user = self.db.get_user_by_name(receiver_input)
|
||||
if not user:
|
||||
return f"❌ 未找到用户: {receiver_input}"
|
||||
receiver_id = user['user_id']
|
||||
|
||||
# 继续原有的赠送逻辑...
|
||||
```
|
||||
|
||||
### 更新帮助信息
|
||||
在 _get_gift_help() 中更新示例:
|
||||
```
|
||||
".gift username 50 生日快乐"
|
||||
".gift 123456 50" (仍支持用户ID)
|
||||
```
|
||||
|
||||
# 任务进度
|
||||
|
||||
## 2025-10-29_15:42:51
|
||||
- 已修改:.tasks/2025-10-29_1_add-user-register.md
|
||||
- 更改:创建任务文件
|
||||
- 原因:开始实施用户注册系统
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
## 2025-10-29_15:46:38
|
||||
- 已修改:
|
||||
- core/database.py(添加 get_user_by_name() 和 update_user_name() 方法,添加 username 索引)
|
||||
- utils/parser.py(添加 .register 和 .注册 命令映射)
|
||||
- routers/callback.py(添加 register 命令处理和 handle_register_command() 函数)
|
||||
- games/gift.py(修改 _process_gift_command() 支持用户名查找,更新帮助信息)
|
||||
- 更改:
|
||||
- 数据库层:添加用户名称查询和更新功能
|
||||
- 命令解析:注册 .register 命令映射
|
||||
- 路由处理:实现用户注册逻辑
|
||||
- 赠送系统:支持通过用户名或ID进行积分赠送
|
||||
- 原因:实现用户注册系统,让用户可以使用名称替代用户ID
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
## 2025-10-29_16:00:48
|
||||
- 已修改:utils/parser.py
|
||||
- 更改:
|
||||
- 调整 COMMAND_MAP 顺序,将 .register 放在 .r 之前(避免前缀匹配冲突)
|
||||
- 修复 AT_PATTERN 正则表达式:从 `@\s*\S+\s+(.+)` 改为 `@[^\s]+\s+(.+)`(正确提取@后的完整指令)
|
||||
- 原因:修复 .register 被错误识别为 dice 的问题,以及@前缀处理不完整的问题
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
# 最终审查
|
||||
[等待实施]
|
||||
|
||||
259
.tasks/2025-10-29_2_complete-adventure-game.md
Normal file
259
.tasks/2025-10-29_2_complete-adventure-game.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# 背景
|
||||
文件名:2025-10-29_2_complete-adventure-game.md
|
||||
创建于:2025-10-29_17:31:02
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:main
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
完善冒险游戏的 `_perform_adventure` 函数,实现冒险系统的时间管理和奖励发放功能。完成后将冒险系统关联到炼金游戏检测中,并将该游戏注册到指令系统中。
|
||||
|
||||
## 关联影响
|
||||
本次更新同时也修改了炼金系统(`games/alchemy.py`),添加了冒险状态检测功能,实现游戏互斥机制。由于炼金系统开发时没有创建独立的任务文件,本次修改记录在本任务文件中。如需追踪炼金系统的完整变更历史,可参考本任务文件的"炼金游戏集成"部分。
|
||||
|
||||
## 核心需求
|
||||
1. 完善 `_perform_adventure` 函数,实现三种状态处理:
|
||||
- 检查用户是否已有未完成的冒险
|
||||
- 如果冒险已完成,发放奖励并清除状态
|
||||
- 如果没有冒险状态,开始新的冒险
|
||||
2. 修复 `_draw_prize` 函数,支持三元组奖品池结构
|
||||
3. 在炼金游戏中添加冒险状态检测,阻止冒险期间进行炼金
|
||||
4. 在指令解析器中注册冒险指令(`.adventure` 和 `.冒险`)
|
||||
5. 在路由处理器中注册冒险游戏
|
||||
6. 在帮助系统中添加冒险游戏的帮助信息
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── games/
|
||||
│ ├── adventure.py # 冒险系统游戏(本次修改)
|
||||
│ ├── alchemy.py # 炼金系统游戏(添加冒险检测)
|
||||
│ └── base.py # 游戏基类(添加帮助信息)
|
||||
├── routers/
|
||||
│ └── callback.py # Callback路由处理(注册冒险游戏)
|
||||
└── utils/
|
||||
└── parser.py # 指令解析(注册冒险指令)
|
||||
```
|
||||
|
||||
# 分析
|
||||
|
||||
## 当前状态
|
||||
1. `games/adventure.py` 中 `_perform_adventure` 函数只有框架,所有逻辑都是 `if False: pass`
|
||||
2. `_draw_prize` 函数期望4元组奖品池,但实际奖品池是3元组 `(权重, 倍率, 描述)`
|
||||
3. 炼金游戏中没有检测用户是否在冒险中
|
||||
4. 指令系统未注册冒险游戏
|
||||
5. 帮助系统未包含冒险游戏说明
|
||||
|
||||
## 关键技术点
|
||||
1. **状态管理**:使用 `game_states` 表存储冒险状态,使用 `chat_id=0` 作为用户级标识
|
||||
2. **时间计算**:使用 Unix 时间戳计算冒险开始和结束时间,以分钟为单位
|
||||
3. **奖品池结构**:修复为支持三元组格式 `(权重, 倍率, 描述)`
|
||||
4. **游戏互斥**:冒险期间禁止炼金操作
|
||||
5. **指令注册**:完整集成到指令解析和路由系统
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. **完善冒险核心逻辑**:
|
||||
- 使用 `game_states` 表存储状态:`{'start_time': timestamp, 'cost_time': minutes}`
|
||||
- 实现三种情况的状态判断和处理
|
||||
- 时间计算:`end_time = start_time + cost_time * 60`
|
||||
2. **修复奖品池处理**:
|
||||
- 修改 `_draw_prize` 支持三元组:`(权重, 倍率, 描述)`
|
||||
- 返回格式:`{'value': 倍率, 'description': 描述}`
|
||||
3. **炼金游戏集成**:
|
||||
- 在 `_perform_alchemy` 开始时检查冒险状态
|
||||
- 冒险进行中时阻止操作并显示剩余时间
|
||||
- 冒险已完成时自动清理状态
|
||||
4. **系统注册**:
|
||||
- 在 `parser.py` 中添加指令映射
|
||||
- 在 `callback.py` 中添加游戏处理器
|
||||
- 在 `base.py` 中添加帮助信息
|
||||
|
||||
# 任务进度
|
||||
|
||||
## 2025-10-29_17:31:02
|
||||
- 已修改:
|
||||
- games/adventure.py(完善 `_perform_adventure` 函数,修复 `_draw_prize` 函数,添加 time 导入)
|
||||
- games/alchemy.py(添加冒险状态检测,添加 time 导入)
|
||||
- utils/parser.py(添加 `.adventure` 和 `.冒险` 指令映射)
|
||||
- routers/callback.py(添加冒险游戏处理分支)
|
||||
- games/base.py(在帮助系统中添加冒险游戏说明)
|
||||
- 更改:
|
||||
1. **冒险系统核心功能**:
|
||||
- 添加 `import time` 模块
|
||||
- 修改 `handle` 方法传递 `chat_id` 参数
|
||||
- 完善 `_perform_adventure` 方法,实现完整的状态管理逻辑:
|
||||
* 参数验证:确保 `cost_time >= 1`
|
||||
* 状态查询:使用 `chat_id=0` 查询用户级冒险状态
|
||||
* 未完成冒险:计算并显示剩余时间(分钟和秒)
|
||||
* 已完成冒险:发放奖励(倍率 × 消耗时间),清除状态
|
||||
* 新冒险:创建状态并保存,显示预计完成时间
|
||||
- 修复 `_draw_prize` 方法支持三元组奖品池
|
||||
2. **炼金游戏集成**:
|
||||
- 添加 `import time` 模块
|
||||
- 在 `_perform_alchemy` 方法开始处添加冒险状态检测
|
||||
- 冒险进行中时返回错误提示并显示剩余时间
|
||||
- 冒险已完成时自动清理状态,允许继续炼金
|
||||
3. **指令系统注册**:
|
||||
- 在 `utils/parser.py` 的 `COMMAND_MAP` 中添加 `.adventure` 和 `.冒险` 映射
|
||||
- 在 `routers/callback.py` 的 `handle_command` 函数中添加冒险游戏处理分支
|
||||
4. **帮助系统更新**:
|
||||
- 在 `games/base.py` 的 `get_help_message` 函数中添加冒险系统帮助信息
|
||||
- 原因:实现冒险系统完整功能,包括时间管理、奖励发放、游戏互斥和系统集成
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## 2025-10-30_00:00:00
|
||||
- 已修改:
|
||||
- games/adventure.py(新增放弃指令分支,新增 `_abandon_adventure`,更新帮助)
|
||||
- games/base.py(在全局帮助中添加放弃用法)
|
||||
- 更改:
|
||||
1. 新增冒险放弃功能:
|
||||
- 支持指令:`.adventure abandon`、`.adventure 放弃`
|
||||
- 结算规则:最低倍率 × 已冒险分钟(向下取整,至少1分钟)
|
||||
- 发放奖励后删除状态
|
||||
2. 帮助信息更新:
|
||||
- 本地帮助与全局帮助均加入放弃用法说明
|
||||
- 原因:允许用户在冒险过程中主动放弃并按最低倍率获得奖励
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## 2025-10-31_10:27:59
|
||||
- 已修改:
|
||||
- games/alchemy.py(修复冒险任务完成后自动删除状态导致奖励丢失的bug)
|
||||
- 更改:
|
||||
1. **Bug修复**:修复冒险任务完成后奖励丢失问题
|
||||
- **问题**:在 `_perform_alchemy` 中,当检测到冒险任务已完成时,代码会自动删除冒险状态(`self.db.delete_game_state`),但没有发放奖励,导致用户奖励丢失
|
||||
- **修复**:移除自动删除逻辑,改为提示用户先使用 `.adventure` 回收奖励
|
||||
- **修改前**:冒险完成后自动删除状态,允许炼金(导致奖励丢失)
|
||||
- **修改后**:冒险完成后提示用户先回收奖励,不允许炼金,确保奖励只能通过 `.adventure` 命令回收
|
||||
2. **行为变更**:
|
||||
- 冒险进行中:提示剩余时间,不允许炼金(保持不变)
|
||||
- 冒险已完成:提示先回收奖励,不允许炼金(修复后)
|
||||
- 用户使用 `.adventure`:发放奖励并删除状态(保持不变)
|
||||
- 状态已删除:可以正常炼金(保持不变)
|
||||
- 原因:修复冒险任务完成后自动删除状态导致奖励丢失的严重bug,确保用户必须先主动回收奖励才能继续其他操作
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 详细实施记录
|
||||
|
||||
## 文件修改清单
|
||||
|
||||
### 1. games/adventure.py
|
||||
- **添加导入**:`import time`
|
||||
- **修改方法签名**:
|
||||
- `handle`:传递 `chat_id` 给 `_perform_adventure`
|
||||
- `_perform_adventure`:添加 `chat_id` 参数,改为 `async`
|
||||
- **完善 `_perform_adventure` 逻辑**:
|
||||
- 参数验证:`cost_time >= 1`
|
||||
- 使用 `get_game_state(0, user_id, 'adventure')` 查询状态
|
||||
- 实现三种状态处理:
|
||||
1. 未完成:计算剩余时间,格式化显示(X分Y秒)
|
||||
2. 已完成:执行抽奖,发放奖励(倍率 × 消耗时间),删除状态
|
||||
3. 新冒险:创建状态数据,保存到数据库,计算预计完成时间
|
||||
- 异常处理:捕获状态数据异常,自动清理损坏状态
|
||||
- **修复 `_draw_prize` 方法**:
|
||||
- 修改循环:`for weight, multiplier, description in prize_pool:`
|
||||
- 返回值:`{'value': multiplier, 'description': description}`
|
||||
- 兜底返回:使用 `prize_pool[0][1]` 和 `prize_pool[0][2]`
|
||||
|
||||
### 2. games/alchemy.py
|
||||
- **添加导入**:`import time`
|
||||
- **在 `_perform_alchemy` 中添加冒险检测**:
|
||||
- 使用 `get_game_state(0, user_id, 'adventure')` 查询状态
|
||||
- 如果存在状态:
|
||||
* 计算剩余时间
|
||||
* 如果已完成:提示用户先使用 `.adventure` 回收奖励,不允许炼金(2025-10-31修复:避免奖励丢失,移除自动删除逻辑)
|
||||
* 如果未完成:返回错误消息,显示剩余时间(X分Y秒)
|
||||
- 异常处理:捕获状态数据异常,自动清理损坏状态
|
||||
|
||||
### 3. utils/parser.py
|
||||
- **在 `COMMAND_MAP` 中添加**:
|
||||
```python
|
||||
# 冒险系统
|
||||
'.adventure': 'adventure',
|
||||
'.冒险': 'adventure',
|
||||
```
|
||||
|
||||
### 4. routers/callback.py
|
||||
- **在 `handle_command` 函数中添加**:
|
||||
```python
|
||||
# 冒险系统
|
||||
if game_type == 'adventure':
|
||||
from games.adventure import AdventureGame
|
||||
game = AdventureGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
### 5. games/base.py
|
||||
- **在 `get_help_message` 函数中添加**:
|
||||
```markdown
|
||||
### ⚡️ 冒险系统
|
||||
- `.adventure` - 消耗1分钟进行冒险
|
||||
- `.冒险` - 消耗1分钟进行冒险
|
||||
- `.adventure 5` - 消耗5分钟进行冒险
|
||||
- `.adventure help` - 查看冒险帮助
|
||||
```
|
||||
|
||||
## 关键实现细节
|
||||
|
||||
### 状态数据结构
|
||||
```python
|
||||
state_data = {
|
||||
'start_time': int(time.time()), # Unix时间戳(秒)
|
||||
'cost_time': 5 # 消耗时间(分钟)
|
||||
}
|
||||
```
|
||||
|
||||
### 时间计算逻辑
|
||||
- 开始时间:`start_time = int(time.time())`
|
||||
- 结束时间:`end_time = start_time + cost_time * 60`
|
||||
- 剩余时间:`remaining_seconds = end_time - current_time`
|
||||
- 剩余时间显示:`remaining_minutes = remaining_seconds // 60`,`remaining_secs = remaining_seconds % 60`
|
||||
|
||||
### 奖励计算
|
||||
- 抽奖获取倍率:`reward = self._draw_prize(prize_pool)`
|
||||
- 奖励积分:`reward_points = int(reward['value'] * cost_time)`
|
||||
- 发放奖励:`self.db.add_points(user_id, reward_points, "adventure", "冒险奖励")`
|
||||
|
||||
### 游戏互斥机制
|
||||
- 炼金前检查:查询冒险状态
|
||||
- 如果冒险进行中:返回错误,显示剩余时间
|
||||
- 如果冒险已完成:提示用户先使用 `.adventure` 回收奖励,不允许炼金(修复后:确保奖励不会丢失)
|
||||
- 状态异常:自动清理,允许继续操作
|
||||
|
||||
# 最终审查
|
||||
|
||||
## 功能验证
|
||||
- ✅ 冒险开始:用户可以指定时间(分钟)开始冒险
|
||||
- ✅ 冒险进行中:显示剩余时间,阻止重复开始
|
||||
- ✅ 冒险完成:自动发放奖励,清除状态
|
||||
- ✅ 时间计算:正确计算剩余时间和完成时间
|
||||
- ✅ 奖励发放:根据倍率和消耗时间计算奖励积分
|
||||
- ✅ 游戏互斥:冒险期间阻止炼金操作
|
||||
- ✅ 指令注册:`.adventure` 和 `.冒险` 指令正常工作
|
||||
- ✅ 帮助信息:显示在全局帮助中
|
||||
- ✅ 冒险放弃:`.adventure abandon` / `.adventure 放弃` 按最低倍率结算已冒险分钟并清理状态
|
||||
|
||||
## 代码质量
|
||||
- ✅ 所有语法检查通过
|
||||
- ✅ 错误处理完善(参数验证、状态异常处理)
|
||||
- ✅ 日志记录完整
|
||||
- ✅ 代码风格一致
|
||||
- ✅ 不了解释清晰
|
||||
|
||||
## 集成完成
|
||||
- ✅ 指令解析器:已注册指令映射
|
||||
- ✅ 路由处理器:已添加游戏处理分支
|
||||
- ✅ 帮助系统:已添加帮助信息
|
||||
- ✅ 游戏互斥:已集成到炼金系统
|
||||
|
||||
**实施与计划完全匹配**
|
||||
|
||||
所有功能已按计划完成,冒险系统已完整集成到WPS Bot Game系统中。
|
||||
|
||||
428
.tasks/2025-10-29_3_ai_chat.md
Normal file
428
.tasks/2025-10-29_3_ai_chat.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# 背景
|
||||
文件名:2025-10-29_3_ai_chat.md
|
||||
创建于:2025-10-29_23:32:40
|
||||
创建者:user
|
||||
主分支:main
|
||||
任务分支:task/ai_chat_2025-10-29_3
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在本项目中新增一个AI对话功能,使用llama_index构建,服务商为本地部署的ollama。
|
||||
|
||||
这个智能体将拥有足够长的上下文(超过30轮对话历史),能够同时与不同的用户展开交流。例如用户A提问后用户B进行补充,智能体将通过时间间隔判断机制来决定何时进行回答。
|
||||
|
||||
## 核心需求
|
||||
1. **显式指令触发**:使用 `.ai <问题>` 指令触发AI对话
|
||||
2. **配置指令**:使用 `.aiconfig` 指令配置Ollama服务地址、端口和模型名称
|
||||
3. **时间间隔判断**:智能体通过时间间隔判断是否需要回答(固定10秒等待窗口)
|
||||
4. **长上下文管理**:保留超过30轮对话历史
|
||||
5. **多用户对话支持**:同一chat_id下不同用户的消息能够被正确识别和处理
|
||||
|
||||
## 技术方案决策(已确定)
|
||||
1. **延迟任务机制**:使用 asyncio 的延迟任务(方案三)
|
||||
- 每个 chat_id 维护独立的延迟任务句柄
|
||||
- 使用全局字典存储任务映射
|
||||
- 收到新消息时取消旧任务并创建新任务
|
||||
|
||||
2. **上下文管理**:使用 llama_index 的 ChatMemoryBuffer(策略A)
|
||||
- 设置足够的 token_limit 确保保留30+轮对话
|
||||
- 按 chat_id 独立维护 ChatEngine 实例
|
||||
|
||||
3. **多用户识别**:消息角色映射 + 系统提示(方案C)
|
||||
- 将不同用户映射为不同角色(如"用户1"、"用户2")
|
||||
- 在系统提示中明确告知存在多用户场景
|
||||
- ChatMemoryBuffer 中使用角色区分不同用户
|
||||
|
||||
4. **等待窗口**:固定10秒窗口(变体1)
|
||||
- 收到消息后等待10秒
|
||||
- 等待期间有新消息则重新计时
|
||||
|
||||
5. **配置管理**:使用单独的JSON文件
|
||||
- 配置存储在 `data/ai_config.json`
|
||||
- 全局单一配置(服务器级别,非chat级别)
|
||||
- 通过 `.aiconfig` 指令修改配置并保存到文件
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBot/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/ # 核心模块
|
||||
│ ├── database.py # 数据库操作
|
||||
│ ├── models.py # 数据模型
|
||||
│ └── middleware.py # 中间件
|
||||
├── routers/ # 路由模块
|
||||
│ ├── callback.py # 回调处理
|
||||
│ └── health.py # 健康检查
|
||||
├── games/ # 游戏模块
|
||||
│ ├── base.py # 游戏基类
|
||||
│ └── ... # 其他游戏
|
||||
├── utils/ # 工具模块
|
||||
│ ├── message.py # 消息发送
|
||||
│ ├── parser.py # 指令解析
|
||||
│ └── rate_limit.py # 限流控制
|
||||
└── data/ # 数据文件
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
- FastAPI:Web框架
|
||||
- SQLite:数据存储
|
||||
- llama-index-core:AI对话框架核心
|
||||
- llama-index-llms-ollama:Ollama LLM集成
|
||||
- Ollama:本地LLM服务
|
||||
|
||||
# 分析
|
||||
|
||||
## 现有架构
|
||||
1. **指令处理流程**:
|
||||
- 消息通过 `/api/callback` 接收
|
||||
- `CommandParser` 解析指令,只处理以 `.` 开头的命令
|
||||
- 非指令消息会被忽略
|
||||
- 指令分发到对应的游戏处理器
|
||||
|
||||
2. **状态管理**:
|
||||
- 游戏状态存储在 `game_states` 表
|
||||
- 使用 `(chat_id, user_id, game_type)` 作为联合主键
|
||||
- 对于群组共享状态,使用 `user_id=0`(如成语接龙)
|
||||
|
||||
3. **异步任务**:
|
||||
- 已有 `periodic_cleanup` 后台清理任务示例
|
||||
- 使用 `asyncio.create_task` 和 `asyncio.sleep` 实现
|
||||
|
||||
## 关键技术挑战
|
||||
|
||||
### 1. 延迟回答机制
|
||||
需要实现一个基于时间间隔的判断机制:
|
||||
- 收到 `.ai` 指令时,将消息加入等待队列
|
||||
- 设置一个等待窗口(例如5-10秒)
|
||||
- 如果在等待窗口内有新消息,重新计时
|
||||
- 等待窗口结束后,如果没有新消息,生成回答
|
||||
- 需要在 `chat_id` 级别维护等待队列和延迟任务
|
||||
|
||||
### 2. 长上下文管理
|
||||
- 使用 llama_index 的 `ChatMemoryBuffer` 管理对话历史
|
||||
- 确保超过30轮对话历史能够被保留
|
||||
- 对话历史需要按 `chat_id` 独立存储
|
||||
- 对话历史中需要包含用户ID信息,以便区分不同用户
|
||||
|
||||
### 3. Ollama配置管理
|
||||
- 使用全局单一配置(服务器级别)
|
||||
- 配置存储在 `data/ai_config.json` 文件中
|
||||
- 配置包括:服务地址、端口、模型名称
|
||||
- 通过 `.aiconfig` 指令修改配置并持久化到文件
|
||||
- 配置需要有默认值(localhost:11434,默认模型需指定)
|
||||
|
||||
### 4. 多用户对话识别
|
||||
- 对话历史中需要记录每条消息的发送者(user_id)
|
||||
- 生成回复时,需要识别上下文中的不同用户
|
||||
- 回复格式可以考虑使用 @用户 的方式
|
||||
|
||||
### 5. 依赖管理
|
||||
- 需要添加 llama-index-core 和相关依赖
|
||||
- 需要确保与现有代码库的兼容性
|
||||
- 考虑资源占用(内存、CPU)
|
||||
|
||||
## 数据结构设计
|
||||
|
||||
### AI对话状态数据结构
|
||||
对话状态由 llama_index 的 ChatMemoryBuffer 管理,存储在内存中。
|
||||
需要存储的额外信息:
|
||||
|
||||
```python
|
||||
# 存储在 game_states 表中的 state_data
|
||||
{
|
||||
"user_mapping": { # 用户ID到角色名称的映射
|
||||
"123456": "用户1",
|
||||
"789012": "用户2",
|
||||
...
|
||||
},
|
||||
"user_count": 2 # 当前对话中的用户数量
|
||||
}
|
||||
```
|
||||
|
||||
### 配置数据结构(存储在 data/ai_config.json)
|
||||
```json
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 11434,
|
||||
"model": "llama3.1"
|
||||
}
|
||||
```
|
||||
|
||||
### 数据库扩展
|
||||
使用 `game_states` 表存储用户映射信息:
|
||||
- `chat_id`: 会话ID
|
||||
- `user_id`: 0(表示群组级别)
|
||||
- `game_type`: "ai_chat"
|
||||
- `state_data`: JSON格式的用户映射信息
|
||||
|
||||
注意:对话历史由 ChatMemoryBuffer 在内存中管理,不持久化到数据库。
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. 创建一个新的游戏模块 `games/ai_chat.py`,继承 `BaseGame`
|
||||
2. 使用 `game_states` 表存储用户映射信息(用户ID到角色名称的映射)
|
||||
3. 使用全局字典维护每个 `chat_id` 的延迟任务句柄
|
||||
4. 使用全局字典维护每个 `chat_id` 的 ChatEngine 实例和待处理消息队列
|
||||
5. 使用 `data/ai_config.json` 存储 Ollama 全局配置
|
||||
6. 使用 llama_index 的 ChatMemoryBuffer 管理对话上下文(内存中)
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 1. 指令注册
|
||||
在 `utils/parser.py` 中添加:
|
||||
- `.ai`: 触发AI对话
|
||||
- `.aiconfig`: 配置Ollama参数
|
||||
|
||||
### 2. AI对话模块 (`games/ai_chat.py`)
|
||||
- `handle()`: 主处理函数,处理 `.ai` 和 `.aiconfig` 指令
|
||||
- `_handle_ai()`: 处理AI对话请求
|
||||
- 将消息加入等待队列
|
||||
- 取消旧的延迟任务(如果存在)
|
||||
- 创建新的延迟任务(10秒后执行)
|
||||
- `_handle_config()`: 处理配置请求
|
||||
- 解析配置参数(host, port, model)
|
||||
- 更新 `data/ai_config.json` 文件
|
||||
- 返回配置确认消息
|
||||
- `_add_to_queue()`: 将消息加入等待队列(按 chat_id 组织)
|
||||
- `_delayed_response()`: 延迟回答任务(内部异步函数)
|
||||
- 等待10秒后执行
|
||||
- 检查队列并生成回答
|
||||
- 处理任务取消异常
|
||||
- `_generate_response()`: 使用LLM生成回答
|
||||
- 获取或创建 ChatEngine 实例
|
||||
- 获取用户角色映射
|
||||
- 将队列中的消息按用户角色格式化
|
||||
- 调用 ChatEngine.chat() 生成回答
|
||||
- 更新 ChatMemoryBuffer
|
||||
- `_get_chat_engine()`: 获取或创建ChatEngine实例
|
||||
- 检查全局字典中是否已存在
|
||||
- 不存在则创建新的 ChatEngine,配置 ChatMemoryBuffer
|
||||
- 设置系统提示(告知多用户场景)
|
||||
- `_get_user_role()`: 获取用户角色名称(创建或获取映射)
|
||||
- `_load_config()`: 从 JSON 文件加载配置
|
||||
- `_save_config()`: 保存配置到 JSON 文件
|
||||
|
||||
### 3. 延迟任务管理
|
||||
- 使用全局字典 `_pending_tasks` 存储每个 `chat_id` 的延迟任务句柄
|
||||
- 使用全局字典 `_message_queues` 存储每个 `chat_id` 的待处理消息队列
|
||||
- 使用全局字典 `_chat_engines` 存储每个 `chat_id` 的 ChatEngine 实例
|
||||
- 新消息到达时,取消旧任务(调用 task.cancel())并创建新任务
|
||||
- 使用 `asyncio.create_task` 和 `asyncio.sleep(10)` 实现固定10秒延迟
|
||||
- 处理 `asyncio.CancelledError` 异常,避免任务取消时的错误日志
|
||||
|
||||
### 4. 用户角色映射机制
|
||||
- 为每个 chat_id 维护用户ID到角色名称的映射(如"用户1"、"用户2")
|
||||
- 映射信息存储在 `game_states` 表中(chat_id, user_id=0, game_type='ai_chat')
|
||||
- 首次出现的用户自动分配角色名称(按出现顺序)
|
||||
- 在将消息添加到 ChatMemoryBuffer 时使用角色名称作为消息角色
|
||||
- 系统提示中包含:"这是一个多用户对话场景,不同用户的发言会用不同的角色标识。你需要理解不同用户的发言内容,并根据上下文给出合适的回复。"
|
||||
|
||||
### 4. 依赖添加
|
||||
在 `requirements.txt` 中添加:
|
||||
```
|
||||
llama-index-core>=0.10.0
|
||||
llama-index-llms-ollama>=0.1.0
|
||||
```
|
||||
|
||||
### 5. 路由注册
|
||||
在 `routers/callback.py` 的 `handle_command()` 中添加AI对话处理分支
|
||||
|
||||
### 6. 帮助信息更新
|
||||
在 `games/base.py` 的 `get_help_message()` 中添加AI对话帮助
|
||||
|
||||
## 时间间隔判断逻辑(固定10秒窗口)
|
||||
1. **默认等待窗口**:10秒(固定)
|
||||
2. **收到 `.ai` 指令时**:
|
||||
- 提取消息内容(去除 `.ai` 前缀)
|
||||
- 获取用户ID和chat_id
|
||||
- 将消息(用户ID + 内容)加入该 `chat_id` 的等待队列
|
||||
- 如果有待处理的延迟任务(检查 `_pending_tasks[chat_id]`),取消它
|
||||
- 创建新的延迟任务(`asyncio.create_task(_delayed_response(chat_id))`)
|
||||
- 将任务句柄存储到 `_pending_tasks[chat_id]`
|
||||
3. **在等待窗口内收到新消息**(无论是否是指令):
|
||||
- 如果新消息也是 `.ai` 指令:
|
||||
- 将新消息加入队列
|
||||
- 取消当前延迟任务(`task.cancel()`)
|
||||
- 创建新的延迟任务(重新计时10秒)
|
||||
- 如果新消息不是指令,但chat_id在等待队列中:
|
||||
- 可以考虑忽略,或也加入队列(根据需求决定)
|
||||
4. **等待窗口结束(延迟任务执行)**:
|
||||
- 检查队列中是否有消息
|
||||
- 如果有,获取该 chat_id 的 ChatEngine 和用户映射
|
||||
- 将队列中的消息按用户角色格式化后添加到 ChatMemoryBuffer
|
||||
- 调用 ChatEngine.chat() 生成回答
|
||||
- 清空队列
|
||||
- 从 `_pending_tasks` 中移除任务句柄
|
||||
|
||||
## 配置文件管理(data/ai_config.json)
|
||||
- 文件结构:
|
||||
```json
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 11434,
|
||||
"model": "llama3.1"
|
||||
}
|
||||
```
|
||||
- 首次加载时如果文件不存在,创建默认配置
|
||||
- 通过 `.aiconfig` 指令修改配置时,实时保存到文件
|
||||
- ChatEngine 创建时从配置文件加载配置
|
||||
|
||||
# 当前执行步骤:"4. 执行模式 - 代码实施完成并测试通过"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## [2025-10-29_23:55:08] 执行阶段完成
|
||||
- 已修改:
|
||||
- requirements.txt:添加 llama-index-core 和 llama-index-llms-ollama 依赖
|
||||
- data/ai_config.json:创建默认配置文件
|
||||
- utils/parser.py:添加 .ai 和 .aiconfig 指令映射和解析逻辑
|
||||
- games/ai_chat.py:创建完整的 AI 对话模块实现
|
||||
- routers/callback.py:添加 ai_chat 处理分支
|
||||
- games/base.py:添加 AI 对话帮助信息
|
||||
- 更改:
|
||||
- 实现了基于 llama_index 和 Ollama 的 AI 对话功能
|
||||
- 实现了固定10秒等待窗口的延迟回答机制
|
||||
- 实现了用户角色映射和长上下文管理
|
||||
- 实现了配置文件的 JSON 存储和管理
|
||||
- 原因:按照计划实施 AI 对话功能的所有核心组件
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## [2025-10-30_00:56:44] 功能优化和问题修复
|
||||
- 已修改:
|
||||
- games/ai_chat.py:优化错误处理和用户体验
|
||||
1. 移除收到消息后的确认回复(静默处理)
|
||||
2. 修复转义字符警告(SyntaxWarning)
|
||||
3. 改进错误处理,提供详细的调试信息和排查步骤
|
||||
4. 添加超时设置(120秒)
|
||||
5. 针对NPS端口转发的特殊错误提示
|
||||
- 更改:
|
||||
- 优化了错误提示信息,包含当前配置、测试命令和详细排查步骤
|
||||
- 专门针对NPS端口转发场景添加了Ollama监听地址配置说明
|
||||
- 改进了连接错误的诊断能力
|
||||
- 原因:根据实际使用中发现的问题进行优化
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## [2025-10-30_01:10:05] 系统提示词持久化和功能完善
|
||||
- 已修改:
|
||||
- games/ai_chat.py:
|
||||
1. 实现系统提示词的持久化存储(保存到配置文件)
|
||||
2. 添加 `_get_default_system_prompt()` 方法定义默认系统提示词
|
||||
3. 添加 `_get_system_prompt()` 方法从配置文件加载系统提示词
|
||||
4. 更新系统提示词内容,明确AI身份和职责
|
||||
5. 在系统提示词中包含完整的机器人功能列表和指引
|
||||
- 更改:
|
||||
- 系统提示词现在会保存到 `data/ai_config.json` 文件中
|
||||
- 服务重启后系统提示词会自动从配置文件加载,保持长期记忆
|
||||
- AI助手能够了解自己的身份和所有机器人功能,可以主动指引用户
|
||||
- 系统提示词包含了完整的13个功能模块介绍和回复指南
|
||||
- 原因:实现系统提示词的长期记忆,让AI能够始终记住自己的身份和职责
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 最终审查
|
||||
|
||||
## 实施总结
|
||||
✅ 所有计划功能已成功实施并通过测试
|
||||
|
||||
### 核心功能实现
|
||||
1. ✅ AI对话系统基于 llama_index + Ollama 构建
|
||||
2. ✅ 显式指令触发(`.ai <问题>`)
|
||||
3. ✅ 配置指令(`.aiconfig`)支持动态配置Ollama服务
|
||||
4. ✅ 固定10秒等待窗口的延迟回答机制
|
||||
5. ✅ 用户角色映射和长上下文管理(30+轮对话)
|
||||
6. ✅ 配置文件持久化存储
|
||||
7. ✅ 系统提示词持久化存储(新增)
|
||||
8. ✅ 完善的错误处理和调试信息
|
||||
|
||||
### 文件修改清单
|
||||
- ✅ requirements.txt - 添加依赖
|
||||
- ✅ data/ai_config.json - 配置文件(包含系统提示词)
|
||||
- ✅ utils/parser.py - 指令解析
|
||||
- ✅ games/ai_chat.py - AI对话模块完整实现
|
||||
- ✅ routers/callback.py - 路由注册
|
||||
- ✅ games/base.py - 帮助信息更新
|
||||
|
||||
### 技术特性
|
||||
- ✅ 多用户对话支持
|
||||
- ✅ 延迟任务管理(asyncio)
|
||||
- ✅ ChatMemoryBuffer长上下文管理
|
||||
- ✅ JSON配置文件管理
|
||||
- ✅ NPS端口转发支持
|
||||
- ✅ 详细的错误诊断和排查指南
|
||||
|
||||
### 测试状态
|
||||
- ✅ 功能测试通过
|
||||
- ✅ Ollama服务连接测试通过
|
||||
- ✅ NPS端口转发配置测试通过
|
||||
- ✅ 系统提示词持久化测试通过
|
||||
|
||||
## 实施与计划匹配度
|
||||
实施与计划完全匹配 ✅
|
||||
|
||||
|
||||
## 补充分析:Markdown 渲染与发送通道(2025-10-30)
|
||||
|
||||
### 现状观察
|
||||
|
||||
- `routers/callback.py` 中仅当 `response_text.startswith('#')` 时才通过 `send_markdown()` 发送,否则使用 `send_text()`。这意味着即使 AI 返回了合法的 Markdown,但不以 `#` 开头(例如代码块、列表、表格、普通段落等),也会被按纯文本通道发送,导致下游(WPS 侧)不进行 Markdown 渲染。
|
||||
- `games/ai_chat.py` 的 `_generate_response()` 直接返回 `str(response)`,未对内容类型进行标注或判定,上层仅依赖首字符为 `#` 的启发式判断来选择发送通道。
|
||||
- `utils/message.py` 已具备 `send_markdown()` 与 `send_text()` 两种发送方式,对应 `{"msgtype":"markdown"}` 与 `{"msgtype":"text"}` 消息结构;当前缺少自动识别 Markdown 的逻辑。
|
||||
|
||||
### 影响
|
||||
|
||||
- 当 AI 返回包含 Markdown 元素但非标题(不以 `#` 开头)的内容时,用户端看到的是未渲染的原始 Markdown 文本,表现为“格式不能成功排版”。
|
||||
|
||||
### 待确认问题(不含解决方案,需产品/实现口径)
|
||||
|
||||
1. 目标平台(WPS 机器人)对 Markdown 的要求是否仅需 `msgtype=markdown` 即可渲染?是否存在必须以标题开头的限制?
|
||||
2. 期望策略:
|
||||
- 是否希望“.ai 的所有回复”统一走 Markdown 通道?
|
||||
- 还是需要基于 Markdown 特征进行判定(如代码块、列表、链接、表格、行内格式等)?
|
||||
3. 兼容性:若统一改为 Markdown 通道,是否会影响既有纯文本展示(例如换行、转义、表情)?
|
||||
4. 其他指令模块是否也可能返回 Markdown?若有,是否一并纳入同一策略?
|
||||
|
||||
### 相关代码参照点(路径)
|
||||
|
||||
- `routers/callback.py`:回复通道选择逻辑(基于 `startswith('#')`)
|
||||
- `games/ai_chat.py`:AI 回复内容生成与返回(直接返回字符串)
|
||||
- `utils/message.py`:`send_markdown()` 与 `send_text()` 的消息结构
|
||||
|
||||
|
||||
### 决策结论与范围(2025-10-30)
|
||||
|
||||
- 分支策略:不创建新分支,继续在当前任务上下文内推进。
|
||||
- 发送策略:`.ai` 产生的回复统一按 Markdown 发送。
|
||||
- 影响范围:仅限 AI 对话功能(`.ai`/`ai_chat`),不扩展到其他指令模块。
|
||||
|
||||
|
||||
# 任务进度(补充)
|
||||
|
||||
## [2025-10-30_??:??:??] 标注 Markdown 渲染问题(记录现状与待确认项)
|
||||
- 已修改:
|
||||
- `.tasks/2025-10-29_3_ai_chat.md`:补充“Markdown 渲染与发送通道”分析与待确认清单(仅问题陈述,无解决方案)。
|
||||
- 更改:
|
||||
- 明确当前仅以标题开头触发 Markdown 发送的启发式导致部分 Markdown 未被渲染。
|
||||
- 原因:
|
||||
- 用户反馈“AI 返回内容支持 Markdown,但当前直接当作文本返回导致无法正确排版”。
|
||||
- 阻碍因素:
|
||||
- 目标平台的 Markdown 渲染细节与统一策略选择待确认。
|
||||
- 状态:
|
||||
- 未确认(等待策略口径与平台渲染规范确认)。
|
||||
|
||||
## [2025-10-30_11:40:31] 执行:AI 回复统一按 Markdown 发送(仅限 AI)
|
||||
- 已修改:
|
||||
- `routers/callback.py`:在 `callback_receive()` 的发送阶段,当 `game_type == 'ai_chat'` 且存在 `response_text` 时,无条件调用 `send_markdown(response_text)`;若发送异常,记录日志并回退到 `send_text(response_text)`;其他指令模块继续沿用 `startswith('#')` 的启发式逻辑。
|
||||
- 更改:
|
||||
- 使 `.ai` 产生的回复在 WPS 端稳定触发 Markdown 渲染,不再依赖以 `#` 开头。
|
||||
- 原因:
|
||||
- 对齐“统一按 Markdown 发送(仅限 AI)”的决策,解决 Markdown 文本被当作纯文本发送导致的排版问题。
|
||||
- 阻碍因素:
|
||||
- 暂无。
|
||||
- 状态:
|
||||
- 成功。
|
||||
531
.tasks/2025-10-30_1_add_casino_games.md
Normal file
531
.tasks/2025-10-30_1_add_casino_games.md
Normal file
@@ -0,0 +1,531 @@
|
||||
# 背景
|
||||
文件名:2025-10-30_1_add_casino_games.md
|
||||
创建于:2025-10-30_15:16:56
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/add_casino_games_2025-01-14_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在项目中新增赌场常见游戏,支持多个玩家下注等功能,使用积分系统模拟真实的赌场环境。
|
||||
|
||||
要求:
|
||||
- 指令格式:`.赌场 <游戏类型> <参数>`
|
||||
- 采用模块化设计,便于扩展多种赌场游戏
|
||||
- 支持多人同时下注
|
||||
- 集成现有积分系统
|
||||
- 记录下注和收益数据
|
||||
|
||||
# 项目概览
|
||||
基于WPS协作开放平台的自定义机器人游戏系统。使用FastAPI + SQLite架构,已有完善的积分系统和多款游戏(五子棋、成语接龙等)。需要通过模块化设计添加赌场游戏功能。
|
||||
|
||||
# 分析
|
||||
|
||||
## 现有系统分析
|
||||
1. **积分系统**:已实现 `add_points()` 和 `consume_points()` 方法
|
||||
2. **游戏基类**:`BaseGame` 提供统一的接口
|
||||
3. **路由系统**:通过 `CommandParser` 解析指令,在 `callback.py` 中路由
|
||||
4. **数据库**:SQLite,已有用户表、游戏状态表、统计表
|
||||
|
||||
## 需要新增的内容
|
||||
1. **数据库表**:
|
||||
- `casino_bets`:记录所有下注
|
||||
- `casino_results`:记录游戏结果和结算
|
||||
- `casino_games`:记录游戏房间(可选)
|
||||
|
||||
2. **游戏模块**:
|
||||
- `games/casino.py`:主赌场模块
|
||||
- 第一期支持:大小游戏
|
||||
- 第二期计划:轮盘、二十一点等
|
||||
|
||||
3. **指令映射**:
|
||||
- `.赌场` -> casino 游戏类型
|
||||
- 子指令:`轮盘`、`大小`、`21点` 等
|
||||
|
||||
## 设计要点
|
||||
1. **模块化设计**:每种赌场游戏作为独立类
|
||||
2. **下注流程**:创建房间 -> 玩家下注 -> 结算 -> 分发奖励
|
||||
3. **安全性**:下注前检查积分,结算时原子性操作
|
||||
4. **多玩家支持**:以 chat_id 为单位创建游戏房间
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 指令设计
|
||||
采用 `.赌场 <游戏类型> <操作> <参数>` 的模块化结构:
|
||||
|
||||
### 大小游戏
|
||||
- **庄家开启游戏**:`.赌场 大小 open <最小下注> <最大下注> <赔率>`
|
||||
- 示例:`.赌场 大小 open 10 100 2.0` (最小10分,最大100分,赔率2.0倍)
|
||||
- **玩家下注**:`.赌场 大小 bet <大小/小> <下注金额>`
|
||||
- 示例:`.赌场 大小 bet 大 50` (下注50分压大)
|
||||
- 示例:`.赌场 大小 bet 小 30` (下注30分压小)
|
||||
- **查看状态**:`.赌场 大小 status`
|
||||
- **庄家结算**:`.赌场 大小 settle <结果>`
|
||||
- 示例:`.赌场 大小 settle 大` (开大)
|
||||
- 示例:`.赌场 大小 settle 小` (开小)
|
||||
|
||||
### 轮盘游戏(二期实现)
|
||||
- 暂不实现,等大小游戏完善后再扩展
|
||||
|
||||
### 21点游戏(二期实现)
|
||||
- 暂不实现,等大小游戏完善后再扩展
|
||||
|
||||
## 游戏流程
|
||||
1. 庄家开启游戏(指定下注限额和赔率参数)
|
||||
2. 玩家下注(可多人同时参与)
|
||||
3. 庄家确认结算(手动触发结果)
|
||||
4. 系统自动分发奖励
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 新增表:casino_bets(下注记录表)
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS casino_bets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
game_type TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
bet_type TEXT NOT NULL, -- '大' 或 '小'
|
||||
amount INTEGER NOT NULL,
|
||||
multiplier REAL NOT NULL, -- 赔率
|
||||
status TEXT DEFAULT 'pending', -- pending/settled/cancelled
|
||||
result TEXT, -- 游戏结果
|
||||
win_amount INTEGER, -- 赢得金额
|
||||
created_at INTEGER NOT NULL,
|
||||
settled_at INTEGER,
|
||||
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
||||
)
|
||||
```
|
||||
|
||||
### 新增表:casino_sessions(游戏会话表)
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS casino_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
game_type TEXT NOT NULL,
|
||||
banker_id INTEGER NOT NULL, -- 庄家ID
|
||||
min_bet INTEGER NOT NULL,
|
||||
max_bet INTEGER NOT NULL,
|
||||
multiplier REAL NOT NULL,
|
||||
house_fee REAL DEFAULT 0.05, -- 抽水率,默认5%
|
||||
status TEXT DEFAULT 'open', -- open/settling/closed
|
||||
created_at INTEGER NOT NULL,
|
||||
settled_at INTEGER,
|
||||
UNIQUE(chat_id, game_type, status)
|
||||
)
|
||||
```
|
||||
|
||||
### 索引
|
||||
- `casino_bets(chat_id, game_type, status)` - 快速查询待结算下注
|
||||
- `casino_sessions(chat_id, game_type, status)` - 快速查询活跃游戏
|
||||
|
||||
## 方案对比
|
||||
|
||||
### 方案A:纯数据库表方案(推荐)
|
||||
**优点**:
|
||||
- 数据结构清晰,便于查询统计
|
||||
- 支持历史记录追踪
|
||||
- 并发安全,利用数据库事务
|
||||
- 易于扩展复杂查询
|
||||
|
||||
**缺点**:
|
||||
- 需要维护额外的表结构
|
||||
- 稍微复杂一些
|
||||
|
||||
**决策**:采用此方案
|
||||
|
||||
### 方案B:game_states + JSON方案
|
||||
**优点**:
|
||||
- 复用现有系统
|
||||
- 实现简单
|
||||
|
||||
**缺点**:
|
||||
- 难以进行复杂统计查询
|
||||
- JSON解析性能较差
|
||||
- 数据格式不够规范化
|
||||
|
||||
## 核心实现细节
|
||||
|
||||
### 1. 游戏流程控制
|
||||
- **开启游戏**:检查是否已有活跃游戏,同一chat_id只能有一个进行中的游戏
|
||||
- **下注限制**:检查session状态、下注金额范围、玩家积分
|
||||
- **结算控制**:只有庄家可以结算,结算后自动关闭session
|
||||
|
||||
### 2. 下注流程
|
||||
1. 检查是否有活跃的session
|
||||
2. 检查下注金额是否符合min/max限制
|
||||
3. 检查用户积分是否充足
|
||||
4. 扣除下注金额(consume_points)
|
||||
5. 记录下注到casino_bets表
|
||||
|
||||
### 3. 结算流程
|
||||
1. 验证是否为庄家操作
|
||||
2. 查询所有pending状态的下注
|
||||
3. 计算每个玩家的输赢
|
||||
4. 使用数据库事务确保原子性:
|
||||
- 更新bets状态
|
||||
- 发放/扣除积分
|
||||
- 更新session状态
|
||||
5. 返回结算报告
|
||||
|
||||
### 4. 抽水机制
|
||||
- **抽水率**:5%(可配置,存储在session.house_fee中)
|
||||
- **抽水时机**:从玩家的赢得金额中扣除
|
||||
- **抽水归属**:归系统所有(不返还给庄家)
|
||||
- **计算方式**:
|
||||
- 玩家赢得 = 下注金额 × 赔率
|
||||
- 实际发放 = 赢得金额 × (1 - 抽水率)
|
||||
- 抽水金额 = 赢得金额 × 抽水率
|
||||
|
||||
### 5. 错误处理
|
||||
- 下注时积分不足:给出明确提示
|
||||
- 重复下注:允许(可下多注)
|
||||
- 非法下注金额:给出范围提示
|
||||
- 非庄家尝试结算:拒绝
|
||||
|
||||
## 安全性
|
||||
- 下注前检查积分
|
||||
- 结算时使用数据库事务保证原子性
|
||||
- 抽水机制保护庄家(虽然抽水归系统)
|
||||
- 验证庄家身份
|
||||
- 防止重复结算
|
||||
|
||||
# 详细实施计划
|
||||
|
||||
## 文件1: core/database.py
|
||||
|
||||
### 修改函数: init_tables()
|
||||
在现有表创建之后(约第130行),添加赌场相关表的创建:
|
||||
|
||||
位置:在 `user_points` 表创建之后(约第130行)添加
|
||||
|
||||
```python
|
||||
# 赌场下注记录表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS casino_bets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
game_type TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
bet_type TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
multiplier REAL NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
result TEXT,
|
||||
win_amount INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
settled_at INTEGER,
|
||||
FOREIGN KEY (user_id) REFERENCES users (user_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# 赌场游戏会话表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS casino_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
game_type TEXT NOT NULL,
|
||||
banker_id INTEGER NOT NULL,
|
||||
min_bet INTEGER NOT NULL,
|
||||
max_bet INTEGER NOT NULL,
|
||||
multiplier REAL NOT NULL,
|
||||
house_fee REAL DEFAULT 0.05,
|
||||
status TEXT DEFAULT 'open',
|
||||
created_at INTEGER NOT NULL,
|
||||
settled_at INTEGER,
|
||||
UNIQUE(chat_id, game_type, status)
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建索引
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_casino_bets
|
||||
ON casino_bets(chat_id, game_type, status)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_casino_sessions
|
||||
ON casino_sessions(chat_id, game_type, status)
|
||||
""")
|
||||
```
|
||||
|
||||
### 新增函数: create_casino_session()
|
||||
函数签名:
|
||||
```python
|
||||
def create_casino_session(self, chat_id: int, game_type: str, banker_id: int,
|
||||
min_bet: int, max_bet: int, multiplier: float,
|
||||
house_fee: float = 0.05) -> int:
|
||||
```
|
||||
|
||||
功能:创建新的赌场游戏会话,返回session_id
|
||||
|
||||
### 新增函数: get_active_casino_session()
|
||||
函数签名:
|
||||
```python
|
||||
def get_active_casino_session(self, chat_id: int, game_type: str) -> Optional[Dict]:
|
||||
```
|
||||
|
||||
功能:获取活跃的游戏会话
|
||||
|
||||
### 新增函数: create_casino_bet()
|
||||
函数签名:
|
||||
```python
|
||||
def create_casino_bet(self, chat_id: int, game_type: str, user_id: int,
|
||||
bet_type: str, amount: int, multiplier: float) -> int:
|
||||
```
|
||||
|
||||
功能:创建下注记录,返回bet_id
|
||||
|
||||
### 新增函数: get_pending_bets()
|
||||
函数签名:
|
||||
```python
|
||||
def get_pending_bets(self, chat_id: int, game_type: str) -> List[Dict]:
|
||||
```
|
||||
|
||||
功能:获取待结算的下注列表
|
||||
|
||||
### 新增函数: settle_casino_bets()
|
||||
函数签名:
|
||||
```python
|
||||
def settle_casino_bets(self, chat_id: int, game_type: str, result: str,
|
||||
banker_id: int) -> Dict:
|
||||
```
|
||||
|
||||
功能:结算所有下注,返回结算详情字典(winners, losers, total_win等)
|
||||
|
||||
### 新增函数: close_casino_session()
|
||||
函数签名:
|
||||
```python
|
||||
def close_casino_session(self, chat_id: int, game_type: str):
|
||||
```
|
||||
|
||||
功能:关闭游戏会话
|
||||
|
||||
## 文件2: games/casino.py(新建)
|
||||
|
||||
### 类: CasinoGame
|
||||
继承自 `BaseGame`
|
||||
|
||||
### 方法: __init__()
|
||||
初始化数据库连接
|
||||
|
||||
### 方法: async handle(command, chat_id, user_id) -> str
|
||||
主处理函数,解析指令并调用相应的处理方法
|
||||
|
||||
解析逻辑:
|
||||
- 提取命令参数,格式:`.赌场 <游戏类型> <操作> <参数>`
|
||||
- 识别游戏类型(第一期只支持"大小")
|
||||
- 分发到相应的处理方法
|
||||
|
||||
### 方法: async _handle_bigsmall(command, args, chat_id, user_id) -> str
|
||||
处理大小游戏的各种操作
|
||||
|
||||
支持的操作:
|
||||
- open: 开启游戏
|
||||
- bet: 下注
|
||||
- status: 查看状态
|
||||
- settle: 结算
|
||||
|
||||
### 方法: async _open_bigsmall(args, chat_id, user_id) -> str
|
||||
庄家开启大小游戏
|
||||
|
||||
参数解析:`<最小下注> <最大下注> <赔率>`
|
||||
参数验证和限制
|
||||
|
||||
### 方法: async _bet_bigsmall(args, chat_id, user_id) -> str
|
||||
玩家下注
|
||||
|
||||
参数解析:`<大小/小> <下注金额>`
|
||||
检查session、金额范围、用户积分
|
||||
|
||||
### 方法: async _status_bigsmall(chat_id, game_type) -> str
|
||||
查看当前游戏状态
|
||||
|
||||
### 方法: async _settle_bigsmall(args, chat_id, user_id) -> str
|
||||
庄家结算游戏
|
||||
|
||||
参数解析:`<大/小>`
|
||||
验证庄家身份,结算所有下注
|
||||
|
||||
### 方法: get_help() -> str
|
||||
返回帮助信息
|
||||
|
||||
## 文件3: utils/parser.py
|
||||
|
||||
### 修改: COMMAND_MAP
|
||||
添加赌场指令映射:
|
||||
|
||||
```python
|
||||
# 赌场系统
|
||||
'.赌场': 'casino',
|
||||
'.casino': 'casino',
|
||||
```
|
||||
|
||||
## 文件4: routers/callback.py
|
||||
|
||||
### 修改: async handle_command()
|
||||
在AI对话系统之后(约第209行)添加:
|
||||
|
||||
```python
|
||||
# 赌场系统
|
||||
if game_type == 'casino':
|
||||
from games.casino import CasinoGame
|
||||
game = CasinoGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
## 文件5: games/base.py
|
||||
|
||||
### 修改: get_help_message()
|
||||
在积分赠送系统之后添加赌场游戏帮助:
|
||||
|
||||
```python
|
||||
### 🎰 赌场系统
|
||||
- `.赌场 大小 open <最小> <最大> <赔率>` - 庄家开启大小游戏
|
||||
- `.赌场 大小 bet <大/小> <金额>` - 下注
|
||||
- `.赌场 大小 status` - 查看状态
|
||||
- `.赌场 大小 settle <大/小>` - 庄家结算
|
||||
```
|
||||
|
||||
## 实施清单
|
||||
|
||||
1. 修改 `core/database.py` 的 `init_tables()` 方法,添加赌场表创建和索引
|
||||
2. 在 `core/database.py` 中添加 `create_casino_session()` 方法
|
||||
3. 在 `core/database.py` 中添加 `get_active_casino_session()` 方法
|
||||
4. 在 `core/database.py` 中添加 `create_casino_bet()` 方法
|
||||
5. 在 `core/database.py` 中添加 `get_pending_bets()` 方法
|
||||
6. 在 `core/database.py` 中添加 `settle_casino_bets()` 方法
|
||||
7. 在 `core/database.py` 中添加 `close_casino_session()` 方法
|
||||
8. 创建文件 `games/casino.py`,定义 `CasinoGame` 类
|
||||
9. 在 `games/casino.py` 中实现 `__init__()` 方法
|
||||
10. 在 `games/casino.py` 中实现 `async handle()` 方法
|
||||
11. 在 `games/casino.py` 中实现 `async _handle_bigsmall()` 方法
|
||||
12. 在 `games/casino.py` 中实现 `async _open_bigsmall()` 方法
|
||||
13. 在 `games/casino.py` 中实现 `async _bet_bigsmall()` 方法
|
||||
14. 在 `games/casino.py` 中实现 `async _status_bigsmall()` 方法
|
||||
15. 在 `games/casino.py` 中实现 `async _settle_bigsmall()` 方法
|
||||
16. 在 `games/casino.py` 中实现 `get_help()` 方法
|
||||
17. 修改 `utils/parser.py`,在 COMMAND_MAP 中添加赌场指令映射
|
||||
18. 修改 `routers/callback.py`,在 `handle_command()` 中添加赌场路由
|
||||
19. 修改 `games/base.py`,在 `get_help_message()` 中添加赌场帮助信息
|
||||
20. 测试所有功能点,确保无错误
|
||||
|
||||
# 当前执行步骤:"2. 详细技术规划完成,等待进入实现阶段"
|
||||
|
||||
# 任务进度
|
||||
[2025-10-30_15:16:56]
|
||||
- 已修改:创建任务文件 `.tasks/2025-10-30_1_add_casino_games.md`
|
||||
- 更改:创建任务分支 `task/add_casino_games_2025-01-14_1` 和任务文件
|
||||
- 原因:按照RIPER-5协议建立工作基础
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-30_16:30:00](预估时间)
|
||||
- 已修改:完成详细技术规划
|
||||
- 更改:设计数据库表结构、游戏流程、抽水机制等细节
|
||||
- 原因:为实施阶段提供详细技术规范
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-30_16:07:57]
|
||||
- 已修改:core/database.py, games/casino.py, utils/parser.py, routers/callback.py, games/base.py
|
||||
- 更改:完成所有实施步骤1-19
|
||||
- 添加赌场表创建和索引
|
||||
- 实现6个数据库方法(create_casino_session, get_active_casino_session, create_casino_bet, get_pending_bets, settle_casino_bets, close_casino_session)
|
||||
- 创建完整的CasinoGame类,实现大小游戏所有功能
|
||||
- 注册指令映射和路由
|
||||
- 添加帮助信息
|
||||
- 原因:按照详细实施计划完成全部功能开发
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-30_17:20:00](预估时间)
|
||||
- 已修改:games/casino.py
|
||||
- 更改:修改结算逻辑,从庄家指定结果改为系统随机生成
|
||||
- 移除庄家输入种子/结果的参数
|
||||
- 使用random.random()生成随机结果(50%大/50%小)
|
||||
- 更新帮助信息,settle命令不再需要参数
|
||||
- 原因:用户反馈庄家不应该能够操控游戏结果,庄家也是玩家
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-30_17:26:19]
|
||||
- 已修改:games/casino.py, games/base.py
|
||||
- 更改:添加庄家放弃游戏功能
|
||||
- 新增_cancel_bigsmall()方法处理放弃逻辑
|
||||
- 放弃时返还所有玩家下注
|
||||
- 关闭会话并标记下注为cancelled
|
||||
- 添加cancel命令支持(cancel/放弃/关闭)
|
||||
- 更新帮助信息和base.py中的帮助
|
||||
- 原因:用户要求庄家可以放弃本轮游戏
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_11:35:18]
|
||||
- 已修改:core/database.py
|
||||
- 更改:扩展数据库支持轮盘和21点游戏
|
||||
- 添加列存在性检查辅助方法(_column_exists, _add_column_if_not_exists)
|
||||
- 扩展casino_sessions表:添加current_phase和blackjack_multiplier字段(兼容性检查)
|
||||
- 扩展casino_bets表:添加bet_category、bet_number、bet_value、hand_status字段(兼容性检查)
|
||||
- 创建casino_blackjack_hands表:存储21点游戏手牌数据
|
||||
- 修改create_casino_session():支持单场限制检查(get_any_active_casino_session)和新字段
|
||||
- 扩展create_casino_bet():支持轮盘和21点专用字段参数
|
||||
- 添加21点手牌管理方法:create_blackjack_hand、get_blackjack_hand、update_blackjack_hand、get_all_blackjack_hands
|
||||
- 原因:为轮盘和21点游戏提供数据库支持,确保字段分离和向后兼容
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_15:15:08]
|
||||
- 已修改:core/database.py
|
||||
- 更改:修复大小游戏结算时的UNIQUE约束冲突问题
|
||||
- 移除casino_sessions表的UNIQUE(chat_id, game_type, status)约束
|
||||
- 原因:status='closed'时需要允许多条历史记录,UNIQUE约束阻止了结算时更新status
|
||||
- 添加兼容性迁移逻辑:检测旧版本表结构,自动重建表以移除UNIQUE约束
|
||||
- 迁移时复制所有历史数据,处理外键关系(临时禁用/启用外键检查)
|
||||
- 单场限制通过应用层逻辑(get_any_active_casino_session)保证
|
||||
- 原因:用户测试大小游戏结算时遇到"UNIQUE constraint failed"错误
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_15:15:08]
|
||||
- 已修改:core/database.py
|
||||
- 更改:修复21点游戏结算逻辑问题
|
||||
- 修正losers统计逻辑:将条件从`not player_is_busted and player_points != banker_points`改为`player_points != banker_points`
|
||||
- 原因:原条件排除了爆牌玩家,导致爆牌玩家未被统计到losers列表
|
||||
- 修正数据库更新逻辑:明确区分三种情况
|
||||
- 赢家:发放奖励并更新数据库
|
||||
- 平局(player_points == banker_points):已返还下注,更新数据库
|
||||
- 输家(else分支,包括爆牌和点数小于庄家):更新数据库
|
||||
- 改进结果字符串显示:包含玩家和庄家的状态信息(爆牌、黑杰克等)
|
||||
- 例如:"庄家19点 vs 玩家爆牌" 或 "庄家19点 vs 玩家20点(黑杰克)"
|
||||
- 原因:用户测试21点游戏时发现3人游戏中只有1个赢家被结算,1个爆牌玩家和1个平局玩家未被结算
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_17:24:08]
|
||||
- 已修改:games/casino.py
|
||||
- 更改:重构21点游戏指令流程,改为更符合标准的玩法
|
||||
- 修改_open_blackjack:改为`.赌场 21点 open <底注> <黑杰克倍数>`,移除max_bet参数
|
||||
- 新增_join_blackjack:添加`.赌场 21点 join`指令,玩家加入游戏时扣除底注,检查积分是否足够
|
||||
- 修改_bet_blackjack:改为加注功能,仅在playing阶段可用,加注金额必须不低于底注
|
||||
- 修改_deal_blackjack:实现标准发牌顺序(先玩家1张→庄家明牌→玩家第2张→庄家暗牌),庄家隐藏一张暗牌
|
||||
- 修改_status_blackjack:游戏阶段隐藏庄家暗牌,只显示明牌,结算后显示完整手牌
|
||||
- 修改_stand_blackjack:检查所有玩家是否都已完成(停牌或爆牌),如果所有玩家都完成则自动触发结算
|
||||
- 修改_hit_blackjack:如果爆牌后所有玩家都完成,也自动触发结算
|
||||
- 更新_get_blackjack_help:反映新的指令流程和规则
|
||||
- 原因:用户要求新的指令流程:启动(open)→加入(join)→发牌(deal)→操作(hit/stand/bet加注)→自动结算
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_17:24:08]
|
||||
- 已修改:games/casino.py
|
||||
- 更改:修复停牌和要牌功能中的字典键访问错误
|
||||
- 修复_hit_blackjack中自动结算检查:将`player_hand['hand_status']`改为`player_hand['status']`
|
||||
- 修复_stand_blackjack中自动结算检查:将`player_hand['hand_status']`改为`player_hand['status']`
|
||||
- 原因:`get_all_blackjack_hands`返回的字典结构为`{user_id: {'cards': [...], 'status': ...}}`,应使用`status`而不是`hand_status`
|
||||
- 原因:用户测试停牌功能时遇到KeyError: 'hand_status'错误
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 最终审查
|
||||
待完成
|
||||
93
.tasks/2025-10-31_1_change-adventure-time-to-seconds.md
Normal file
93
.tasks/2025-10-31_1_change-adventure-time-to-seconds.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 背景
|
||||
文件名:2025-10-31_1_change-adventure-time-to-seconds.md
|
||||
创建于:2025-10-31_17:30:37
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:main
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
将冒险系统的计时单位从分钟更改为秒。需要修改所有相关的时间计算、验证逻辑、显示文本和帮助信息。
|
||||
|
||||
## 后续需求
|
||||
支持时间格式输入(如 `1h30m10s`),并在显示时按时分秒格式显示,避免只显示秒数过于冗余。
|
||||
|
||||
# 项目概览
|
||||
修改 `games/adventure.py` 文件,将冒险时间单位从分钟改为秒。
|
||||
|
||||
# 分析
|
||||
当前代码中所有时间相关逻辑都以分钟为单位:
|
||||
- 默认值:1分钟
|
||||
- 参数验证:至少1分钟
|
||||
- 时间计算:`cost_time * 60` 转换为秒
|
||||
- 显示文本:所有提示信息都显示"分钟"
|
||||
- 奖励计算:倍率 × 分钟数
|
||||
- 放弃冒险:按分钟计算已消耗时间
|
||||
|
||||
需要将所有相关逻辑改为秒为单位。
|
||||
|
||||
# 提议的解决方案
|
||||
将所有时间单位从分钟改为秒:
|
||||
1. 修改默认值和参数验证
|
||||
2. 移除时间计算中的 `* 60` 转换
|
||||
3. 更新所有显示文本
|
||||
4. 简化剩余时间显示逻辑(直接显示秒)
|
||||
5. 更新帮助信息
|
||||
|
||||
# 当前执行步骤:"已完成"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## 2025-10-31_17:32:32
|
||||
- 已修改:games/adventure.py
|
||||
- 更改:
|
||||
1. 修改默认值注释:从"1分钟"改为"1秒"
|
||||
2. 修改函数文档:cost_time参数从"(分钟)"改为"(秒)"
|
||||
3. 修改参数验证:错误提示从"至少需要1分钟"改为"至少需要1秒"
|
||||
4. 移除时间转换:将`end_time = start_time + saved_cost_time * 60`改为`end_time = start_time + saved_cost_time`
|
||||
5. 更新显示文本:所有"分钟"改为"秒"(冒险结果、冒险进行中、冒险开始、冒险放弃)
|
||||
6. 简化剩余时间显示:移除分钟/秒的转换逻辑,直接显示秒数
|
||||
7. 修改放弃冒险逻辑:时间计算改为直接使用秒数(elapsed_seconds),移除分钟转换
|
||||
8. 更新帮助信息:所有"分钟"改为"秒"
|
||||
- 原因:将冒险系统计时单位从分钟改为秒,使时间控制更精确
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## 2025-10-31_17:35:06
|
||||
- 已修改:games/adventure.py
|
||||
- 更改:
|
||||
1. 添加 `re` 模块导入,用于正则表达式解析
|
||||
2. 新增 `_parse_time_string` 方法:解析时间格式字符串,支持以下格式:
|
||||
- 纯数字(按秒):`60` -> 60秒
|
||||
- 时分秒组合:`1h30m10s` -> 5410秒
|
||||
- 分钟秒组合:`30m10s` -> 1810秒
|
||||
- 只有小时:`1h` -> 3600秒
|
||||
- 只有分钟:`30m` -> 1800秒
|
||||
- 只有秒:`10s` -> 10秒
|
||||
3. 新增 `_format_time` 方法:将秒数格式化为 "X时X分X秒" 格式,自动省略为0的部分
|
||||
4. 修改 `handle` 方法:使用 `_parse_time_string` 解析时间参数,提供格式错误提示
|
||||
5. 更新所有时间显示位置:
|
||||
- 冒险结果:使用 `_format_time` 格式化消耗时间
|
||||
- 冒险进行中:使用 `_format_time` 格式化剩余时间和总时长
|
||||
- 冒险开始:使用 `_format_time` 格式化持续时间
|
||||
- 冒险放弃:使用 `_format_time` 格式化已计入时间
|
||||
6. 更新帮助信息:添加时间格式说明和示例
|
||||
- 原因:支持更灵活的时间输入格式,提升用户体验;时间显示按时分秒格式,避免冗长的秒数显示
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## 2025-10-31_17:49:24
|
||||
- 已修改:games/adventure.py
|
||||
- 更改:
|
||||
1. 修复预计完成时间显示问题:
|
||||
- 原问题:只显示小时时刻(`%H:%M:%S`),跨天的冒险无法正确显示,且秒数显示不够明确
|
||||
- 第一次尝试:根据冒险时长是否超过24小时判断(不准确)
|
||||
- 最终解决方案:根据完成时间是否跨天来判断
|
||||
- 跨天或跨年:显示完整日期时间 `YYYY-MM-DD HH:MM:SS`(包含年月日和时分秒)
|
||||
- 同一天:显示时间 `HH:MM:SS`(包含时分秒)
|
||||
- 原因:修复跨天冒险无法正确显示完成时间的问题,只要跨天就显示完整日期,确保秒数清晰显示
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 最终审查
|
||||
|
||||
480
.tasks/2025-11-03_1_user-webhook-url.md
Normal file
480
.tasks/2025-11-03_1_user-webhook-url.md
Normal file
@@ -0,0 +1,480 @@
|
||||
# 背景
|
||||
文件名:2025-11-03_1_user-webhook-url.md
|
||||
创建于:2025-11-03_09:38:30
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/user-webhook-url_2025-11-03_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在WPS Bot Game项目中添加用户专属webhook URL功能,允许每个用户注册自己的个人webhook URL作为私聊途径。
|
||||
|
||||
## 核心需求
|
||||
1. 用户可以通过 `.register url <url>` 指令注册个人webhook URL
|
||||
2. 私聊消息发送功能将被封装为API接口,供其他系统调用
|
||||
3. 提供检测用户是否具有个人URL的接口,用于系统运行时确保参与用户都能被私聊
|
||||
4. 服务器启动时使用的webhook URL称为主URL,私聊用的URL称为个人URL
|
||||
|
||||
## 术语定义
|
||||
- **主URL**: 服务器启动时使用的webhook URL,用于群聊消息发送
|
||||
- **个人URL**: 用户注册的专属webhook URL,用于私聊消息发送
|
||||
|
||||
## 功能要求
|
||||
1. **注册功能**: 支持 `.register url <url>` 指令注册/更新个人URL
|
||||
2. **私聊接口**: 封装私聊消息发送功能为API接口(暂不对用户开放命令)
|
||||
3. **检测接口**: 提供单个和批量检测用户是否有个人URL的接口
|
||||
4. **数据库支持**: 在users表中添加webhook_url字段
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/
|
||||
│ ├── database.py # SQLite数据库操作
|
||||
│ ├── middleware.py # 中间件
|
||||
│ └── models.py # 数据模型
|
||||
├── routers/
|
||||
│ ├── callback.py # Callback路由处理
|
||||
│ ├── health.py # 健康检查
|
||||
│ └── private.py # 私聊相关API(新增)
|
||||
├── games/ # 游戏模块
|
||||
│ └── ... # 各种游戏
|
||||
└── utils/
|
||||
├── parser.py # 指令解析
|
||||
└── message.py # 消息发送
|
||||
```
|
||||
|
||||
# 分析
|
||||
|
||||
## 当前状态
|
||||
1. `users` 表已有基础字段:user_id, username, created_at, last_active
|
||||
2. `routers/callback.py` 中已有 `.register` 命令处理名称注册
|
||||
3. `utils/message.py` 中的 `MessageSender` 类使用全局webhook URL发送消息
|
||||
4. 数据库已支持动态添加列(`_add_column_if_not_exists`方法)
|
||||
5. `init_tables()` 方法在表创建后会进行兼容性检查,使用 `_add_column_if_not_exists` 安全添加新列
|
||||
|
||||
## 关键技术点
|
||||
1. **数据库层**:
|
||||
- 在`init_tables()`中使用`_add_column_if_not_exists`添加`webhook_url`字段(TEXT类型,可为NULL)
|
||||
- 确保兼容性:如果表已存在且没有该列,会自动添加
|
||||
- 添加`set_user_webhook_url(user_id, webhook_url)`方法
|
||||
- 添加`get_user_webhook_url(user_id)`方法
|
||||
- 添加`has_webhook_url(user_id)`方法
|
||||
- 添加`check_users_webhook_urls(user_ids)`批量检查方法
|
||||
|
||||
2. **注册命令扩展**:
|
||||
- 修改`handle_register_command`支持`.register url <url>`子命令
|
||||
- 保留原有的`.register <name>`功能
|
||||
- URL验证(基本格式检查)
|
||||
|
||||
3. **私聊消息发送**:
|
||||
- 封装私聊消息发送功能到`utils/message.py`
|
||||
- 创建`send_private_message(user_id, content, msg_type='text')`函数
|
||||
- 如果用户有个人URL则使用个人URL,否则返回错误
|
||||
|
||||
4. **API接口**:
|
||||
- 创建`routers/private.py`路由文件
|
||||
- `POST /api/private/send` - 发送私聊消息
|
||||
- `GET /api/private/check/{user_id}` - 检查单个用户是否有个人URL
|
||||
- `POST /api/private/check-batch` - 批量检查多个用户
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. **数据库扩展**: 在users表添加webhook_url字段,并实现相关CRUD方法
|
||||
2. **注册命令扩展**: 扩展`.register`命令支持`url`子命令
|
||||
3. **私聊功能封装**: 创建私聊消息发送工具函数
|
||||
4. **API接口**: 创建私聊相关的RESTful API接口
|
||||
|
||||
## 设计决策
|
||||
- 个人URL存储在users表中,与用户信息关联
|
||||
- 私聊功能暂不提供用户命令,仅作为API接口供系统调用
|
||||
- URL验证采用基本格式检查(http/https开头)
|
||||
- 批量检查接口支持传入用户ID列表,返回每个用户的URL状态
|
||||
|
||||
# 当前执行步骤:"3. 执行阶段完成"
|
||||
|
||||
实施清单:
|
||||
1. 在core/database.py的init_tables()方法末尾添加webhook_url字段兼容性检查
|
||||
2. 在core/database.py中添加set_user_webhook_url方法
|
||||
3. 在core/database.py中添加get_user_webhook_url方法
|
||||
4. 在core/database.py中添加has_webhook_url方法
|
||||
5. 在core/database.py中添加check_users_webhook_urls方法
|
||||
6. 在core/models.py文件末尾添加PrivateMessageRequest模型
|
||||
7. 在core/models.py中添加CheckBatchRequest模型
|
||||
8. 在core/models.py中添加CheckBatchResponse模型
|
||||
9. 在core/models.py的导入中添加List类型
|
||||
10. 修改routers/callback.py的handle_register_command函数支持url子命令
|
||||
11. 在utils/message.py文件末尾添加send_private_message函数
|
||||
12. 创建新文件routers/private.py,包含所有私聊相关API接口
|
||||
13. 在app.py中导入private路由模块
|
||||
14. 在app.py中注册private路由
|
||||
|
||||
# 详细实施计划
|
||||
|
||||
## 文件1: core/database.py
|
||||
|
||||
### 修改点1: 在init_tables()方法中添加webhook_url字段兼容性检查
|
||||
|
||||
**位置**: 在`init_tables()`方法的末尾,第324行`logger.info("数据库表初始化完成")`之前
|
||||
|
||||
**修改内容**:
|
||||
```python
|
||||
# 兼容性检查:为users表添加webhook_url字段
|
||||
self._add_column_if_not_exists('users', 'webhook_url', 'TEXT')
|
||||
```
|
||||
|
||||
### 修改点2: 添加set_user_webhook_url方法
|
||||
|
||||
**位置**: 在`# ===== 用户相关操作 =====`部分,`update_user_name`方法之后(约第414行之后)
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def set_user_webhook_url(self, user_id: int, webhook_url: str) -> bool:
|
||||
"""设置用户webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
webhook_url: Webhook URL
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 使用try-except包装
|
||||
- 确保用户存在(调用get_or_create_user)
|
||||
- UPDATE users SET webhook_url = ? WHERE user_id = ?
|
||||
- 记录成功/失败日志
|
||||
- 返回True/False,异常时返回False
|
||||
|
||||
### 修改点3: 添加get_user_webhook_url方法
|
||||
|
||||
**位置**: 紧接`set_user_webhook_url`方法之后
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def get_user_webhook_url(self, user_id: int) -> Optional[str]:
|
||||
"""获取用户webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
Webhook URL,如果不存在返回None
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- SELECT webhook_url FROM users WHERE user_id = ?
|
||||
- 如果查询结果为None,返回None
|
||||
- 如果webhook_url为None或空字符串,返回None
|
||||
- 否则返回URL字符串
|
||||
|
||||
### 修改点4: 添加has_webhook_url方法
|
||||
|
||||
**位置**: 紧接`get_user_webhook_url`方法之后
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def has_webhook_url(self, user_id: int) -> bool:
|
||||
"""检查用户是否有个人webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
是否有个人URL
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 调用get_user_webhook_url
|
||||
- 检查返回值是否不为None且不为空字符串
|
||||
|
||||
### 修改点5: 添加check_users_webhook_urls方法(批量检查)
|
||||
|
||||
**位置**: 紧接`has_webhook_url`方法之后
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def check_users_webhook_urls(self, user_ids: List[int]) -> Dict[int, bool]:
|
||||
"""批量检查用户是否有个人webhook URL
|
||||
|
||||
Args:
|
||||
user_ids: 用户ID列表
|
||||
|
||||
Returns:
|
||||
字典 {user_id: has_url}
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 如果user_ids为空,返回空字典
|
||||
- 使用IN子句查询:SELECT user_id, webhook_url FROM users WHERE user_id IN (?)
|
||||
- 构建结果字典:初始化为所有user_id为False
|
||||
- 遍历查询结果,如果webhook_url不为None且不为空字符串,则设为True
|
||||
- 返回结果字典
|
||||
|
||||
## 文件2: routers/callback.py
|
||||
|
||||
### 修改点1: 修改handle_register_command函数支持url子命令
|
||||
|
||||
**位置**: 第226-260行的`handle_register_command`函数
|
||||
|
||||
**修改内容**:
|
||||
- 提取命令和参数后,检查第一个参数是否为"url"
|
||||
- 如果是"url",提取URL参数,验证URL格式(http/https开头),调用`db.set_user_webhook_url`
|
||||
- 如果不是"url",保持原有逻辑(注册名称)
|
||||
- 更新帮助信息,包含两种用法
|
||||
|
||||
**新的函数逻辑**:
|
||||
```python
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
# 检查是否为url子命令
|
||||
parts = args.split(maxsplit=1)
|
||||
if parts and parts[0].lower() == 'url':
|
||||
# 处理URL注册
|
||||
if len(parts) < 2:
|
||||
return "❌ 请提供webhook URL!\n\n正确格式:`.register url <URL>`\n\n示例:\n`.register url https://example.com/webhook?key=xxx`"
|
||||
webhook_url = parts[1].strip()
|
||||
# URL验证
|
||||
if not webhook_url.startswith(('http://', 'https://')):
|
||||
return "❌ URL格式无效!必须以 http:// 或 https:// 开头。"
|
||||
# 设置URL
|
||||
db = get_db()
|
||||
success = db.set_user_webhook_url(user_id, webhook_url)
|
||||
if success:
|
||||
return f"✅ Webhook URL注册成功!\n\n**您的个人URL**:{webhook_url}\n\n私聊消息将发送到此URL。"
|
||||
else:
|
||||
return "❌ 注册失败!请稍后重试。"
|
||||
else:
|
||||
# 原有的名称注册逻辑
|
||||
...
|
||||
```
|
||||
|
||||
## 文件3: utils/message.py
|
||||
|
||||
### 修改点1: 添加send_private_message函数
|
||||
|
||||
**位置**: 在文件末尾,`get_message_sender`函数之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
async def send_private_message(user_id: int, content: str, msg_type: str = 'text') -> bool:
|
||||
"""发送私聊消息到用户个人webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 目标用户ID
|
||||
content: 消息内容
|
||||
msg_type: 消息类型 ('text' 或 'markdown')
|
||||
|
||||
Returns:
|
||||
是否发送成功,如果用户没有个人URL则返回False
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 从数据库获取用户webhook URL
|
||||
- 如果URL不存在,记录日志并返回False
|
||||
- 创建MessageSender实例(使用用户的个人URL)
|
||||
- 根据msg_type调用send_text或send_markdown
|
||||
- 返回发送结果
|
||||
|
||||
## 文件4: core/models.py (新增数据模型)
|
||||
|
||||
### 修改点1: 添加PrivateMessageRequest模型
|
||||
|
||||
**位置**: 文件末尾
|
||||
|
||||
**模型定义**:
|
||||
```python
|
||||
class PrivateMessageRequest(BaseModel):
|
||||
"""私聊消息请求模型"""
|
||||
user_id: int = Field(..., description="目标用户ID")
|
||||
content: str = Field(..., description="消息内容")
|
||||
msg_type: str = Field(default="text", description="消息类型: text 或 markdown")
|
||||
```
|
||||
|
||||
### 修改点2: 添加CheckBatchRequest模型
|
||||
|
||||
**位置**: 紧接PrivateMessageRequest之后
|
||||
|
||||
**模型定义**:
|
||||
```python
|
||||
class CheckBatchRequest(BaseModel):
|
||||
"""批量检查请求模型"""
|
||||
user_ids: List[int] = Field(..., description="用户ID列表")
|
||||
```
|
||||
|
||||
### 修改点3: 添加CheckBatchResponse模型
|
||||
|
||||
**位置**: 紧接CheckBatchRequest之后
|
||||
|
||||
**模型定义**:
|
||||
```python
|
||||
class CheckBatchResponse(BaseModel):
|
||||
"""批量检查响应模型"""
|
||||
results: Dict[int, bool] = Field(..., description="用户ID到是否有URL的映射")
|
||||
```
|
||||
|
||||
**注意**: core/models.py需要添加`from typing import List`导入(如果尚未导入)
|
||||
|
||||
## 文件5: routers/private.py (新建文件)
|
||||
|
||||
### 文件结构:
|
||||
```python
|
||||
"""私聊相关API路由"""
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from core.database import get_db
|
||||
from core.models import PrivateMessageRequest, CheckBatchRequest, CheckBatchResponse
|
||||
from utils.message import send_private_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
```
|
||||
|
||||
### 接口1: POST /api/private/send
|
||||
|
||||
**位置**: router定义之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
@router.post("/private/send")
|
||||
async def send_private(request: PrivateMessageRequest):
|
||||
"""发送私聊消息
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_id": 123456,
|
||||
"content": "消息内容",
|
||||
"msg_type": "text" // 可选,默认为"text"
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 验证msg_type(必须是"text"或"markdown"),否则返回400错误
|
||||
- 调用send_private_message
|
||||
- 如果返回False(用户没有个人URL或发送失败),返回400错误和相应消息
|
||||
- 成功则返回JSONResponse({"success": True, "message": "消息发送成功"})
|
||||
|
||||
### 接口2: GET /api/private/check/{user_id}
|
||||
|
||||
**位置**: send_private之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
@router.get("/private/check/{user_id}")
|
||||
async def check_user_webhook(user_id: int):
|
||||
"""检查用户是否有个人webhook URL"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 调用db.has_webhook_url(user_id)
|
||||
- 返回JSONResponse({"user_id": user_id, "has_webhook_url": bool})
|
||||
|
||||
### 接口3: POST /api/private/check-batch
|
||||
|
||||
**位置**: check_user_webhook之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
@router.post("/private/check-batch")
|
||||
async def check_users_webhook_batch(request: CheckBatchRequest):
|
||||
"""批量检查用户是否有个人webhook URL
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_ids": [123456, 789012, ...]
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 调用db.check_users_webhook_urls(request.user_ids)
|
||||
- 返回CheckBatchResponse(results=...)
|
||||
|
||||
## 文件6: app.py
|
||||
|
||||
### 修改点1: 导入private路由
|
||||
|
||||
**位置**: 第13行,导入语句中
|
||||
|
||||
**修改内容**:
|
||||
```python
|
||||
from routers import callback, health, private
|
||||
```
|
||||
|
||||
### 修改点2: 注册private路由
|
||||
|
||||
**位置**: 第75-76行,路由注册部分
|
||||
|
||||
**修改内容**:
|
||||
```python
|
||||
app.include_router(callback.router, prefix="/api", tags=["callback"])
|
||||
app.include_router(health.router, tags=["health"])
|
||||
app.include_router(private.router, prefix="/api", tags=["private"])
|
||||
```
|
||||
|
||||
# 任务进度
|
||||
|
||||
[2025-11-03_09:45:56]
|
||||
- 已修改:
|
||||
1. core/database.py - 添加webhook_url字段兼容性检查和4个数据库方法
|
||||
2. core/models.py - 添加3个API数据模型和List类型导入
|
||||
3. routers/callback.py - 扩展handle_register_command支持url子命令
|
||||
4. utils/message.py - 添加send_private_message函数
|
||||
5. routers/private.py - 新建文件,包含3个私聊相关API接口
|
||||
6. app.py - 导入并注册private路由
|
||||
|
||||
- 更改:
|
||||
1. 在users表中添加webhook_url字段支持(兼容性检查)
|
||||
2. 实现用户webhook URL的CRUD操作(设置、获取、检查、批量检查)
|
||||
3. 扩展.register命令支持`.register url <url>`子命令
|
||||
4. 封装私聊消息发送功能为独立函数
|
||||
5. 创建私聊相关的RESTful API接口(发送、单个检查、批量检查)
|
||||
6. 注册新的API路由到FastAPI应用
|
||||
|
||||
- 原因:
|
||||
实现用户专属webhook URL注册和私聊消息发送功能,为其他系统提供API接口调用
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-03_后续]
|
||||
- 已修改:
|
||||
1. utils/parser.py - 添加.talk和.私聊指令映射
|
||||
2. routers/callback.py - 添加handle_talk_command函数实现私聊指令
|
||||
|
||||
- 更改:
|
||||
1. 添加.talk <username> <content>指令,允许用户通过用户名发送私聊消息
|
||||
2. 实现用户名和URL验证,确保目标用户已注册名称和个人URL
|
||||
3. 私聊消息发送成功时不向主URL发送提示消息,保持私密性
|
||||
|
||||
- 原因:
|
||||
实现用户可用的私聊功能,作为私聊功能的开始
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功(测试通过)
|
||||
|
||||
# 最终审查
|
||||
|
||||
待审查阶段完成...
|
||||
|
||||
395
.tasks/2025-11-03_2_werewolf-game.md
Normal file
395
.tasks/2025-11-03_2_werewolf-game.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# 背景
|
||||
文件名:2025-11-03_2_werewolf-game.md
|
||||
创建于:2025-11-03_12:20:10
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/werewolf_2025-11-03_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在WPS Bot Game项目中添加狼人杀游戏系统,支持6-12人游戏,包含身份分配、私聊功能、技能使用等核心功能。
|
||||
|
||||
## 核心需求
|
||||
1. 支持6-12人狼人杀游戏(配置:2-4狼 1预言家 1女巫 2-6平民)
|
||||
2. 主持人开房:`.狼人杀 open`
|
||||
3. 玩家加入:`.狼人杀 join`(必须注册用户名和个人URL)
|
||||
4. 开始游戏:`.狼人杀 start`,自动分配身份并通过私聊发送
|
||||
5. 私聊功能:`.狼人杀 <玩家代号> <消息>`
|
||||
6. 狼人群聊:`.狼人杀 狼人 <消息>`
|
||||
7. 技能系统:杀、验、救、毒
|
||||
8. 游戏状态查询:`.狼人杀 status`
|
||||
9. 结束游戏:`.狼人杀 end`
|
||||
|
||||
## 游戏规则
|
||||
**人数配置**:
|
||||
- 6人:2狼 1预言家 1女巫 2平民
|
||||
- 8人:2狼 1预言家 1女巫 4平民
|
||||
- 10人:3狼 1预言家 1女巫 5平民
|
||||
- 12人:4狼 1预言家 1女巫 6平民
|
||||
|
||||
**胜利条件**:
|
||||
- 狼人阵营:杀死所有神职和平民
|
||||
- 好人阵营:消灭所有狼人
|
||||
|
||||
**技能**:
|
||||
- 狼人:每晚投票刀人
|
||||
- 预言家:每晚查验一个玩家身份
|
||||
- 女巫:拥有一瓶解药(仅可使用一次)和一瓶毒药(仅可使用一次)
|
||||
- 平民:无特殊技能
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/
|
||||
│ ├── database.py # SQLite数据库操作
|
||||
│ ├── middleware.py # 中间件
|
||||
│ └── models.py # 数据模型
|
||||
├── routers/
|
||||
│ ├── callback.py # Callback路由处理
|
||||
│ ├── health.py # 健康检查
|
||||
│ └── private.py # 私聊相关API
|
||||
├── games/ # 游戏模块
|
||||
│ ├── werewolf.py # 狼人杀游戏(新增)
|
||||
│ └── ... # 其他游戏
|
||||
└── utils/
|
||||
├── parser.py # 指令解析
|
||||
└── message.py # 消息发送
|
||||
```
|
||||
|
||||
# 分析
|
||||
|
||||
## 当前状态
|
||||
1. 已有私聊功能支持,用户可注册个人webhook URL
|
||||
2. 已有游戏架构:BaseGame基类、数据库状态管理
|
||||
3. 已有成语接龙等多人类游戏参考
|
||||
4. 数据库支持game_states表存储游戏状态
|
||||
|
||||
## 关键技术点
|
||||
1. **数据库层**:
|
||||
- 使用game_states表存储游戏状态(user_id=0表示群级别状态)
|
||||
- 通过state_data JSON字段存储玩家列表、身份、阶段等信息
|
||||
|
||||
2. **私聊系统**:
|
||||
- 利用现有的send_private_message函数
|
||||
- 身份信息、技能结果等通过私聊发送
|
||||
- 支持发送者标识显示
|
||||
|
||||
3. **游戏状态管理**:
|
||||
- 游戏状态存储在state_data中
|
||||
- 包含:玩家列表、身份映射、狼人列表、技能使用记录等
|
||||
|
||||
4. **技能系统**:
|
||||
- 狼人刀人:投票机制
|
||||
- 预言家验人:私聊返回结果
|
||||
- 女巫用药:限制使用次数
|
||||
|
||||
5. **指令路由**:
|
||||
- 在parser.py注册.werewolf和.狼人杀指令
|
||||
- 在callback.py中导入并注册游戏处理器
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. **创建狼人杀游戏类**:`games/werewolf.py`,继承BaseGame
|
||||
2. **状态数据结构**:使用JSON存储在state_data中
|
||||
3. **身份分配**:随机分配角色,狼人互相认识
|
||||
4. **私聊通知**:游戏开始时通过私聊发送身份信息
|
||||
5. **技能系统**:支持杀、验、救、毒四种技能
|
||||
6. **指令注册**:添加解析和路由支持
|
||||
|
||||
## 设计决策
|
||||
- 使用user_id=0存储群级别游戏状态(参考成语接龙)
|
||||
- 通过私聊发送敏感信息(身份、技能结果)
|
||||
- 简化实现:主持人手动推进阶段(暂不实现自动流转)
|
||||
- 使用数字代号(1-N)标识玩家
|
||||
- 支持狼人群聊功能
|
||||
|
||||
# 当前执行步骤:"实施完成"
|
||||
|
||||
# 详细实施计划
|
||||
|
||||
## 文件1: games/werewolf.py(新建文件)
|
||||
|
||||
### 主要方法
|
||||
1. **handle()** - 主处理逻辑,指令路由
|
||||
2. **get_help()** - 帮助信息
|
||||
3. **_open_game()** - 主持人开房
|
||||
4. **_join_game()** - 玩家加入
|
||||
5. **_start_game()** - 开始游戏,分配身份
|
||||
6. **_send_identities()** - 私聊发送身份信息
|
||||
7. **_private_chat()** - 玩家私聊
|
||||
8. **_wolf_group_chat()** - 狼人群聊
|
||||
9. **_handle_skill()** - 技能处理
|
||||
10. **_wolf_kill()** - 狼人刀人
|
||||
11. **_seer_check()** - 预言家验人
|
||||
12. **_witch_save()** - 女巫救人
|
||||
13. **_witch_poison()** - 女巫毒人
|
||||
14. **_show_status()** - 显示游戏状态
|
||||
15. **_end_game()** - 结束游戏
|
||||
|
||||
### 数据结构设计
|
||||
```python
|
||||
state_data = {
|
||||
'creator_id': int, # 主持人ID
|
||||
'status': str, # 'open', 'playing', 'finished'
|
||||
'players': [
|
||||
{
|
||||
'user_id': int,
|
||||
'name': str, # 注册的用户名
|
||||
'id': int, # 游戏内代号 1-N
|
||||
'role': str, # 'wolf', 'seer', 'witch', 'civilian'
|
||||
'alive': bool,
|
||||
'id_label': str # "1号玩家"
|
||||
}
|
||||
],
|
||||
'phase': str, # 当前阶段
|
||||
'round': int, # 当前回合数
|
||||
'wolves': [int], # 狼人ID列表
|
||||
'kill_target': int, # 狼人票决目标
|
||||
'seer_result': {}, # 预言家验人结果
|
||||
'witch_save': bool, # 女巫是否救人
|
||||
'witch_poison': int, # 女巫毒杀目标
|
||||
'discussed': False, # 讨论阶段是否完成
|
||||
'wolf_know_each_other': False
|
||||
}
|
||||
```
|
||||
|
||||
## 文件2: utils/parser.py
|
||||
|
||||
### 修改点:添加指令映射
|
||||
在COMMAND_MAP中添加:
|
||||
```python
|
||||
'.werewolf': 'werewolf',
|
||||
'.狼人杀': 'werewolf',
|
||||
```
|
||||
|
||||
## 文件3: routers/callback.py
|
||||
|
||||
### 修改点:添加路由处理
|
||||
在handle_command函数中添加:
|
||||
```python
|
||||
# 狼人杀系统
|
||||
if game_type == 'werewolf':
|
||||
from games.werewolf import WerewolfGame
|
||||
game = WerewolfGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
## 文件4: games/base.py
|
||||
|
||||
### 修改点:添加帮助信息
|
||||
在get_help_message()函数中添加狼人杀帮助说明
|
||||
|
||||
# 任务进度
|
||||
|
||||
[2025-11-03_12:20:10]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 新建狼人杀游戏类,实现所有核心功能
|
||||
2. utils/parser.py - 添加.werewolf和.狼人杀指令映射
|
||||
3. routers/callback.py - 添加狼人杀路由处理
|
||||
4. games/base.py - 添加狼人杀帮助信息
|
||||
|
||||
- 更改:
|
||||
1. 创建完整的狼人杀游戏系统
|
||||
2. 支持开房、加入、开始、私聊、技能使用等所有核心功能
|
||||
3. 实现6-12人游戏配置和角色分配
|
||||
4. 集成私聊系统发送身份信息
|
||||
5. 支持狼人群聊功能
|
||||
6. 添加帮助信息和指令注册
|
||||
|
||||
- 原因:
|
||||
实现完整的狼人杀游戏系统,支持多人游戏、身份隐藏、技能使用等核心功能
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-04_17:41:14]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 改进阶段提示和自动流转功能
|
||||
|
||||
- 更改:
|
||||
1. 添加阶段名称映射系统(phase_configs),定义各阶段的中文名称、行动角色和指令说明
|
||||
2. 实现阶段管理方法:_get_phase_description()、_get_next_phase()、_advance_phase()
|
||||
3. 改进游戏开始提示,明确显示"第一夜 - 狼人行动阶段"和操作指令
|
||||
4. 实现自动阶段流转:狼人刀人后自动进入预言家阶段,预言家验人后自动进入女巫阶段
|
||||
5. 新增女巫跳过功能:支持`.werewolf 跳过`指令,女巫可以选择不行动
|
||||
6. 改进状态显示:_show_status()现在显示中文阶段名称、当前行动角色和操作指令
|
||||
7. 更新身份说明和帮助信息,添加女巫跳过选项说明
|
||||
8. 各技能方法添加阶段验证,确保在正确的阶段使用技能
|
||||
|
||||
- 原因:
|
||||
解决用户反馈的游戏阶段不明显的问题,让玩家清楚知道当前是什么阶段、谁应该行动、下一步是什么阶段
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_10:59:09]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 支持在私聊中使用狼人杀技能(方案2实施)
|
||||
|
||||
- 更改:
|
||||
1. 新增 _find_player_game(user_id) 方法,根据玩家ID查找其参与的游戏
|
||||
2. 修改 _handle_skill() 方法,支持从私聊中使用技能指令
|
||||
3. 修改 _wolf_group_chat() 方法,支持从私聊中发送狼人群聊
|
||||
4. 修改 _private_chat() 方法,支持从私聊中发送玩家私聊
|
||||
5. 修改 _witch_pass() 方法,支持从私聊中跳过女巫行动
|
||||
6. 添加日志输出,显示在私聊中使用功能的情况
|
||||
7. 实现逻辑:先尝试用当前chat_id查找游戏(群聊场景),找不到则通过user_id查找玩家游戏(私聊场景)
|
||||
|
||||
- 原因:
|
||||
解决私聊中无法使用技能的问题。游戏在群里创建,但玩家需要在私聊中使用技能以保密。
|
||||
之前的设计只能在游戏所在群使用技能,现在支持在私聊中使用,查找玩家参与的游戏并操作。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_11:06:58]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 改为全局唯一游戏模式(不再按chat_id区分)
|
||||
|
||||
- 更改:
|
||||
1. 修改 _get_game_state() 方法,查询全局唯一游戏而非根据chat_id查询
|
||||
2. 新增 _get_game_chat_id() 方法,获取全局游戏所在的chat_id
|
||||
3. 简化 _find_player_game() 方法,使用全局游戏查询
|
||||
4. 修改 _open_game() 方法,检查全局是否已有游戏(而非仅检查当前群)
|
||||
5. 简化所有需要查找玩家游戏的方法(_handle_skill、_wolf_group_chat、_private_chat、_witch_pass)
|
||||
6. 保持数据库兼容性:chat_id列仍然存在并记录游戏创建的群,但查询时忽略
|
||||
7. 实现逻辑:所有查询都获取全局最新的一个狼人杀游戏,不再区分群组
|
||||
|
||||
- 原因:
|
||||
Bot全局只需要一个狼人杀游戏,不同群的玩家可以参与同一个游戏。
|
||||
简化逻辑,消除按chat_id区分的复杂性,同时保持数据库结构兼容。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_11:13:56]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 改进狼人投票机制
|
||||
|
||||
- 更改:
|
||||
1. 在游戏状态数据中添加 wolf_votes 字段,记录每个狼人的投票
|
||||
2. 修改 _wolf_kill() 方法,实现完整的投票流程:
|
||||
- 记录每个狼人的投票(支持改票)
|
||||
- 检查是否所有存活狼人都已投票
|
||||
- 未全部投票时提示等待其他狼人
|
||||
- 全部投票后统计票数
|
||||
- 票数唯一时确定目标并推进阶段
|
||||
- 平票时清除投票记录并要求重新投票
|
||||
3. 优化投票提示信息,显示投票进度和结果
|
||||
|
||||
- 原因:
|
||||
解决之前"只要一个狼人投票就立即刀人"的问题。
|
||||
现在要求所有狼人都投票,统计票数最多的目标,平票则重新投票,符合狼人杀游戏规则。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_11:22:53]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 狼人投票结果保密处理
|
||||
|
||||
- 更改:
|
||||
1. 修改狼人投票反馈机制,不在群里播报投票目标
|
||||
2. 单个狼人投票时,通过私聊确认投票,群消息只显示"投票已记录"
|
||||
3. 投票平票时,通过私聊通知狼人具体平票目标,群消息只显示"平票"
|
||||
4. 投票完成时,通过私聊通知所有狼人击杀目标,群消息只显示"投票完成"
|
||||
5. 所有敏感信息(投票目标、平票详情、击杀决定)均通过私聊发送给狼人
|
||||
|
||||
- 原因:
|
||||
符合狼人杀游戏规则,狼人刀人的决定应该保密,不能在群里公开播报。
|
||||
只有狼人自己知道投票情况和击杀目标,其他玩家在天亮时才知道结果。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_11:40:44]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 新增空刀机制
|
||||
|
||||
- 更改:
|
||||
1. 允许狼人投票给0号(表示空刀,不杀人)
|
||||
2. 修改 _wolf_kill() 方法,对target_id=0不验证目标存在性
|
||||
3. 修改投票确认消息,区分空刀和正常投票
|
||||
4. 修改票数统计逻辑,投票0不计入击杀目标统计
|
||||
5. 新增全部空刀处理:如果所有狼人都空刀,kill_target设为0(平安夜)
|
||||
6. 修改 _advance_phase() 为async,进入女巫阶段时私聊通知女巫刀人情况
|
||||
7. 女巫有解药时可知道今晚是否有人被刀(0号为平安夜)
|
||||
|
||||
- 原因:
|
||||
符合狼人杀游戏规则,狼人可以选择空刀(不杀人)。
|
||||
女巫在有解药时需要知道今晚是否有人被刀以决定是否使用解药。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-10_10:20:38]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 调整status指令的房间名单展示
|
||||
|
||||
- 更改:
|
||||
1. 房间开放阶段现在显示已加入玩家的房内ID与用户名
|
||||
2. 游戏进行阶段的玩家状态显示同时包含房内ID和用户名
|
||||
|
||||
- 原因:
|
||||
提升`.werewolf status`指令提供的信息量,方便玩家识别房间成员
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:未确认
|
||||
|
||||
[2025-11-10_10:27:07]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 统一狼人投票记录的键类型
|
||||
|
||||
- 更改:
|
||||
1. `_wolf_kill()` 读取和保存 `wolf_votes` 时转换为字符串键
|
||||
2. 统计投票进度时将键重新转换为整数,确保与 `alive_wolves` 对齐
|
||||
|
||||
- 原因:
|
||||
修复狼人全部投票后仍提示有人未投票的问题
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:未确认
|
||||
|
||||
[2025-11-10_10:36:39]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 修复狼人投票流程与阶段推进
|
||||
|
||||
- 更改:
|
||||
1. `_wolf_kill()` 读取 `wolf_votes` 时统一转换为整数键,存储与统计均使用整数
|
||||
2. `_wolf_kill()`、`_seer_check()`、`_witch_save()`、`_witch_poison()`、`_witch_pass()` 中的 `_advance_phase` 调用改为 `await`
|
||||
|
||||
- 原因:
|
||||
修复狼人投票完成后仍判定未投票、平安夜提示错误及阶段推进信息显示 `<coroutine>` 的问题
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:未确认
|
||||
|
||||
# 最终审查
|
||||
|
||||
待审查阶段完成...
|
||||
|
||||
175
README.md
175
README.md
@@ -29,6 +29,41 @@
|
||||
- 3次答题机会
|
||||
- 关键词智能匹配
|
||||
|
||||
### 🀄 成语接龙
|
||||
- 支持多音字和谐音接龙
|
||||
- 全局黑名单系统
|
||||
- 实时状态显示
|
||||
- 参与者统计排行
|
||||
- 支持指定下一位接龙者
|
||||
|
||||
### ⚫ 五子棋
|
||||
- 标准15×15棋盘
|
||||
- 黑方禁手规则(三三、四四、长连)
|
||||
- 多轮对战同时进行
|
||||
- 完整的战绩统计
|
||||
- 实时棋盘显示
|
||||
|
||||
### 💎 积分系统
|
||||
- 每日签到获得固定积分(10分)
|
||||
- 运势占卜随机获得积分(1-20分,30%概率)
|
||||
- 个人积分查询和记录
|
||||
- 积分排行榜
|
||||
- 完整的积分变动记录
|
||||
|
||||
### ⚗️ 炼金系统
|
||||
- 消耗积分进行抽奖(10/20/50积分)
|
||||
- 奖品池数学期望略高于消耗积分,对玩家友好
|
||||
- 包含大奖(巨额积分)和负面奖励(额外扣分)
|
||||
- 完整的炼金记录和统计
|
||||
- 支持多种消耗档位的抽奖
|
||||
|
||||
### 🎁 积分赠送系统
|
||||
- 用户间积分赠送功能
|
||||
- 支持附赠个性化消息
|
||||
- 完整的赠送和接收记录
|
||||
- 赠送统计和记录查询
|
||||
- 单次最多赠送1000积分
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
@@ -50,26 +85,62 @@ cd WPSBotGame
|
||||
# 使用conda环境
|
||||
conda activate liubai
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 注意:成语接龙游戏需要pypinyin库进行拼音处理
|
||||
```
|
||||
|
||||
3. **配置环境变量**
|
||||
3. **配置Webhook**
|
||||
有三种方式配置Webhook URL:
|
||||
|
||||
#### 方式1:命令行参数(推荐)
|
||||
```bash
|
||||
# Linux/Mac
|
||||
python app.py --webhook-url "https://xz.wps.cn/api/v1/webhook/send?key=your_key"
|
||||
|
||||
# Windows
|
||||
python app.py --webhook-url "https://xz.wps.cn/api/v1/webhook/send?key=your_key"
|
||||
```
|
||||
|
||||
#### 方式2:使用启动脚本
|
||||
```bash
|
||||
# Linux/Mac
|
||||
./start.sh -w "https://xz.wps.cn/api/v1/webhook/send?key=your_key"
|
||||
|
||||
# Windows
|
||||
start.bat -w "https://xz.wps.cn/api/v1/webhook/send?key=your_key"
|
||||
```
|
||||
|
||||
#### 方式3:环境变量
|
||||
```bash
|
||||
# 复制配置文件模板
|
||||
cp env.example .env
|
||||
cp config.env.example config.env
|
||||
|
||||
# 编辑配置文件,填入你的Webhook URL
|
||||
nano .env
|
||||
nano config.env
|
||||
```
|
||||
|
||||
4. **运行应用**
|
||||
```bash
|
||||
# 开发模式
|
||||
# 基本启动
|
||||
python app.py
|
||||
|
||||
# 自定义参数启动
|
||||
python app.py --webhook-url "your_webhook_url" --port 8080 --log-level debug
|
||||
|
||||
# 使用启动脚本
|
||||
./start.sh -w "your_webhook_url" -p 8080 -l debug
|
||||
|
||||
# 生产模式(使用uvicorn)
|
||||
uvicorn app:app --host 0.0.0.0 --port 8000 --workers 1
|
||||
```
|
||||
|
||||
### 命令行参数说明
|
||||
- `--webhook-url, -w`: WPS Webhook URL
|
||||
- `--host, -H`: 服务器主机地址 (默认: 0.0.0.0)
|
||||
- `--port, -p`: 服务器端口 (默认: 11000)
|
||||
- `--workers`: 工作进程数 (默认: 1)
|
||||
- `--log-level`: 日志级别 (默认: info)
|
||||
|
||||
## 📝 配置说明
|
||||
|
||||
### 环境变量
|
||||
@@ -90,6 +161,13 @@ MESSAGE_RATE_LIMIT=20
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# 游戏配置(可选,使用默认值)
|
||||
# 成语接龙最大历史显示数量
|
||||
IDIOM_MAX_HISTORY_DISPLAY=10
|
||||
|
||||
# 五子棋最大并发游戏数
|
||||
GOMOKU_MAX_CONCURRENT_GAMES=5
|
||||
```
|
||||
|
||||
### WPS机器人配置
|
||||
@@ -148,6 +226,65 @@ LOG_LEVEL=INFO
|
||||
.quiz 答案 # 回答问题
|
||||
```
|
||||
|
||||
### 成语接龙
|
||||
|
||||
```
|
||||
.idiom start [成语] # 开始游戏(可指定起始成语)
|
||||
.idiom [成语] # 接龙
|
||||
.idiom [成语] @某人 # 接龙并指定下一位
|
||||
.idiom stop # 结束游戏
|
||||
.idiom status # 查看游戏状态
|
||||
.idiom reject [词语] # 拒绝词语加入黑名单(仅发起人)
|
||||
.idiom blacklist # 查看黑名单
|
||||
.idiom next @某人 # 指定下一位(仅最后接龙者)
|
||||
```
|
||||
|
||||
### 五子棋
|
||||
|
||||
```
|
||||
.gomoku challenge # 发起挑战
|
||||
.gomoku accept # 接受挑战
|
||||
.gomoku A1 # 在A1位置落子
|
||||
.gomoku show # 显示当前棋盘
|
||||
.gomoku resign # 认输
|
||||
.gomoku cancel # 取消自己的挑战
|
||||
.gomoku list # 列出所有进行中的对战
|
||||
.gomoku stats # 查看个人战绩
|
||||
```
|
||||
|
||||
### 积分系统
|
||||
|
||||
```
|
||||
.points # 查看个人积分
|
||||
.积分 # 查看个人积分
|
||||
.checkin # 每日签到
|
||||
.签到 # 每日签到
|
||||
.打卡 # 每日签到
|
||||
.points leaderboard # 积分排行榜
|
||||
```
|
||||
|
||||
### 炼金系统
|
||||
|
||||
```
|
||||
.alchemy # 消耗10积分进行炼金
|
||||
.炼金 # 消耗10积分进行炼金
|
||||
.alchemy 20 # 消耗20积分进行炼金
|
||||
.alchemy 50 # 消耗50积分进行炼金
|
||||
.alchemy stats # 查看炼金统计
|
||||
.alchemy records # 查看炼金记录
|
||||
```
|
||||
|
||||
### 积分赠送系统
|
||||
|
||||
```
|
||||
.gift 123 50 生日快乐 # 赠送50积分给用户123,附赠消息
|
||||
.赠送 456 100 感谢帮助 # 赠送100积分给用户456,附赠消息
|
||||
.送 789 200 # 赠送200积分给用户789
|
||||
.gift stats # 查看赠送统计
|
||||
.gift sent # 查看发送记录
|
||||
.gift received # 查看接收记录
|
||||
```
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
@@ -155,6 +292,7 @@ WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── requirements.txt # Python依赖
|
||||
├── env.example # 环境变量模板
|
||||
├── core/ # 核心模块
|
||||
│ ├── database.py # SQLite数据库
|
||||
│ ├── models.py # 数据模型
|
||||
@@ -167,15 +305,31 @@ WPSBotGame/
|
||||
│ ├── parser.py # 指令解析
|
||||
│ └── rate_limit.py # 限流控制
|
||||
├── games/ # 游戏模块
|
||||
│ ├── base.py # 游戏基类
|
||||
│ ├── dice.py # 骰娘系统
|
||||
│ ├── rps.py # 石头剪刀布
|
||||
│ ├── fortune.py # 运势占卜
|
||||
│ ├── guess.py # 猜数字
|
||||
│ └── quiz.py # 问答游戏
|
||||
└── data/ # 数据文件
|
||||
├── bot.db # SQLite数据库
|
||||
├── fortunes.json # 运势数据
|
||||
└── quiz.json # 问答题库
|
||||
│ ├── quiz.py # 问答游戏
|
||||
│ ├── idiom.py # 成语接龙
|
||||
│ ├── gomoku.py # 五子棋
|
||||
│ ├── gomoku_logic.py # 五子棋逻辑
|
||||
│ ├── points.py # 积分系统
|
||||
│ ├── alchemy.py # 炼金系统
|
||||
│ └── gift.py # 积分赠送系统
|
||||
├── data/ # 数据文件
|
||||
│ ├── bot.db # SQLite数据库
|
||||
│ ├── fortunes.json # 运势数据
|
||||
│ ├── quiz.json # 问答题库
|
||||
│ └── idiom_blacklist.json # 成语黑名单
|
||||
├── deploy/ # 部署配置
|
||||
│ ├── install.sh # 安装脚本
|
||||
│ ├── manage.sh # 管理脚本
|
||||
│ ├── nginx/ # Nginx配置
|
||||
│ └── systemd/ # systemd服务配置
|
||||
└── Convention/ # 通用工具库
|
||||
├── Runtime/ # 运行时工具
|
||||
└── Image/ # 图像处理工具
|
||||
```
|
||||
|
||||
## 🔧 部署
|
||||
@@ -256,11 +410,12 @@ curl http://localhost:8000/stats
|
||||
|
||||
## 📈 性能指标
|
||||
|
||||
- **内存占用**:150-250MB
|
||||
- **内存占用**:150-300MB(包含拼音处理库)
|
||||
- **响应时间**:<500ms
|
||||
- **并发支持**:5-10个同时请求
|
||||
- **用户规模**:50-100个活跃用户
|
||||
- **消息限制**:20条/分钟(WPS限制)
|
||||
- **支持游戏**:10种游戏类型(骰子、石头剪刀布、运势、猜数字、问答、成语接龙、五子棋、积分系统、炼金系统、积分赠送系统)
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
|
||||
243
STARTUP_GUIDE.md
Normal file
243
STARTUP_GUIDE.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# WPS Bot Game 启动方式说明
|
||||
|
||||
## 🚀 多种启动方式
|
||||
|
||||
### 1. 命令行参数启动(推荐)
|
||||
|
||||
#### 基本启动
|
||||
```bash
|
||||
python app.py --webhook-url "https://xz.wps.cn/api/v1/webhook/send?key=your_key"
|
||||
```
|
||||
|
||||
#### 自定义参数启动
|
||||
```bash
|
||||
python app.py \
|
||||
--webhook-url "https://xz.wps.cn/api/v1/webhook/send?key=your_key" \
|
||||
--host 0.0.0.0 \
|
||||
--port 8080 \
|
||||
--workers 1 \
|
||||
--log-level debug
|
||||
```
|
||||
|
||||
#### 短参数形式
|
||||
```bash
|
||||
python app.py -w "https://xz.wps.cn/api/v1/webhook/send?key=your_key" -p 8080 -l debug
|
||||
```
|
||||
|
||||
### 2. 使用启动脚本
|
||||
|
||||
#### Linux/Mac
|
||||
```bash
|
||||
# 基本启动
|
||||
./start.sh -w "https://xz.wps.cn/api/v1/webhook/send?key=your_key"
|
||||
|
||||
# 自定义参数
|
||||
./start.sh -w "your_webhook_url" -p 8080 -l debug
|
||||
|
||||
# 查看帮助
|
||||
./start.sh --help
|
||||
```
|
||||
|
||||
#### Windows
|
||||
```cmd
|
||||
REM 基本启动
|
||||
start.bat -w "https://xz.wps.cn/api/v1/webhook/send?key=your_key"
|
||||
|
||||
REM 自定义参数
|
||||
start.bat -w "your_webhook_url" -p 8080 -l debug
|
||||
|
||||
REM 查看帮助
|
||||
start.bat --help
|
||||
```
|
||||
|
||||
### 3. 环境变量启动
|
||||
|
||||
#### 设置环境变量
|
||||
```bash
|
||||
# Linux/Mac
|
||||
export WEBHOOK_URL="https://xz.wps.cn/api/v1/webhook/send?key=your_key"
|
||||
export HOST="0.0.0.0"
|
||||
export PORT="8080"
|
||||
export LOG_LEVEL="debug"
|
||||
|
||||
# Windows
|
||||
set WEBHOOK_URL=https://xz.wps.cn/api/v1/webhook/send?key=your_key
|
||||
set HOST=0.0.0.0
|
||||
set PORT=8080
|
||||
set LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
#### 启动应用
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 4. 配置文件启动
|
||||
|
||||
#### 创建配置文件
|
||||
```bash
|
||||
# 复制配置模板
|
||||
cp config.env.example config.env
|
||||
|
||||
# 编辑配置文件
|
||||
nano config.env
|
||||
```
|
||||
|
||||
#### 配置文件内容
|
||||
```env
|
||||
WEBHOOK_URL=https://xz.wps.cn/api/v1/webhook/send?key=your_key
|
||||
HOST=0.0.0.0
|
||||
PORT=8080
|
||||
WORKERS=1
|
||||
LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
#### 启动应用
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
## 📋 参数说明
|
||||
|
||||
| 参数 | 短参数 | 默认值 | 说明 |
|
||||
|------|--------|--------|------|
|
||||
| `--webhook-url` | `-w` | 无 | WPS Webhook URL |
|
||||
| `--host` | `-H` | `0.0.0.0` | 服务器主机地址 |
|
||||
| `--port` | `-p` | `11000` | 服务器端口 |
|
||||
| `--workers` | 无 | `1` | 工作进程数 |
|
||||
| `--log-level` | `-l` | `info` | 日志级别 |
|
||||
|
||||
## 🔧 生产环境部署
|
||||
|
||||
### 使用systemd服务
|
||||
```bash
|
||||
# 编辑服务文件
|
||||
sudo nano /etc/systemd/system/wps-bot.service
|
||||
|
||||
# 服务文件内容
|
||||
[Unit]
|
||||
Description=WPS Bot Game
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your_user
|
||||
WorkingDirectory=/path/to/WPSBotGame
|
||||
ExecStart=/usr/bin/python3 app.py --webhook-url "your_webhook_url" --port 11000
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### 使用Docker
|
||||
```dockerfile
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "app.py", "--webhook-url", "your_webhook_url"]
|
||||
```
|
||||
|
||||
### 使用Nginx反向代理
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:11000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 1. 端口被占用
|
||||
```bash
|
||||
# 查看端口占用
|
||||
netstat -tlnp | grep :11000
|
||||
|
||||
# 杀死占用进程
|
||||
sudo kill -9 PID
|
||||
|
||||
# 或使用其他端口
|
||||
python app.py --port 8080
|
||||
```
|
||||
|
||||
### 2. 权限问题
|
||||
```bash
|
||||
# 给启动脚本执行权限
|
||||
chmod +x start.sh
|
||||
|
||||
# 检查文件权限
|
||||
ls -la start.sh
|
||||
```
|
||||
|
||||
### 3. 依赖问题
|
||||
```bash
|
||||
# 检查Python版本
|
||||
python --version
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 检查依赖
|
||||
python -c "import fastapi, uvicorn"
|
||||
```
|
||||
|
||||
### 4. 网络问题
|
||||
```bash
|
||||
# 测试Webhook URL
|
||||
curl -X POST "your_webhook_url" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"msg_type": "text", "content": {"text": "test"}}'
|
||||
|
||||
# 检查防火墙
|
||||
sudo ufw status
|
||||
sudo ufw allow 11000
|
||||
```
|
||||
|
||||
## 📊 监控和日志
|
||||
|
||||
### 查看日志
|
||||
```bash
|
||||
# 实时查看日志
|
||||
tail -f /var/log/wps-bot.log
|
||||
|
||||
# 查看系统日志
|
||||
journalctl -u wps-bot -f
|
||||
|
||||
# 查看错误日志
|
||||
grep ERROR /var/log/wps-bot.log
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
```bash
|
||||
# 检查服务状态
|
||||
curl http://localhost:11000/health
|
||||
|
||||
# 检查统计信息
|
||||
curl http://localhost:11000/stats
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
```bash
|
||||
# 查看进程状态
|
||||
ps aux | grep python
|
||||
|
||||
# 查看内存使用
|
||||
free -h
|
||||
|
||||
# 查看磁盘使用
|
||||
df -h
|
||||
```
|
||||
46
app.py
46
app.py
@@ -1,14 +1,16 @@
|
||||
"""WPS Bot Game - FastAPI主应用"""
|
||||
import logging
|
||||
import argparse
|
||||
import os
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from contextlib import asynccontextmanager
|
||||
import asyncio
|
||||
|
||||
from config import APP_CONFIG, SESSION_TIMEOUT
|
||||
from config import APP_CONFIG, SESSION_TIMEOUT, SetWebhookURL, GetWebhookURL
|
||||
from core.middleware import ConcurrencyLimitMiddleware
|
||||
from core.database import get_db
|
||||
from routers import callback, health
|
||||
from routers import callback, health, private
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
@@ -72,6 +74,7 @@ app.add_middleware(ConcurrencyLimitMiddleware)
|
||||
# 注册路由
|
||||
app.include_router(callback.router, prefix="/api", tags=["callback"])
|
||||
app.include_router(health.router, tags=["health"])
|
||||
app.include_router(private.router, prefix="/api", tags=["private"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@@ -94,14 +97,45 @@ async def global_exception_handler(request, exc):
|
||||
)
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(description='WPS Bot Game')
|
||||
parser.add_argument('--webhook-url', '-w',
|
||||
help='WPS Webhook URL')
|
||||
parser.add_argument('--host', '-H',
|
||||
default='0.0.0.0',
|
||||
help='服务器主机地址 (默认: 0.0.0.0)')
|
||||
parser.add_argument('--port', '-p',
|
||||
type=int, default=11000,
|
||||
help='服务器端口 (默认: 11000)')
|
||||
parser.add_argument('--workers',
|
||||
type=int, default=1,
|
||||
help='工作进程数 (默认: 1)')
|
||||
parser.add_argument('--log-level',
|
||||
default='info',
|
||||
choices=['debug', 'info', 'warning', 'error'],
|
||||
help='日志级别 (默认: info)')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
# 解析命令行参数
|
||||
args = parse_args()
|
||||
|
||||
# 如果提供了webhook URL,设置环境变量
|
||||
if args.webhook_url:
|
||||
SetWebhookURL(args.webhook_url)
|
||||
logger.info(f"设置Webhook URL: {args.webhook_url}")
|
||||
|
||||
# 启动服务器
|
||||
uvicorn.run(
|
||||
"app:app",
|
||||
host="0.0.0.0",
|
||||
port=11000,
|
||||
workers=1,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
workers=args.workers,
|
||||
limit_concurrency=5,
|
||||
log_level="info"
|
||||
log_level=args.log_level
|
||||
)
|
||||
|
||||
|
||||
21
config.env.example
Normal file
21
config.env.example
Normal file
@@ -0,0 +1,21 @@
|
||||
# WPS Bot Game 配置文件示例
|
||||
# 复制此文件为 config.env 并修改相应配置
|
||||
|
||||
# WPS Webhook URL (必需)
|
||||
# 格式: https://xz.wps.cn/api/v1/webhook/send?key=your_key
|
||||
WEBHOOK_URL=https://xz.wps.cn/api/v1/webhook/send?key=your_key_here
|
||||
|
||||
# 服务器配置
|
||||
HOST=0.0.0.0
|
||||
PORT=11000
|
||||
WORKERS=1
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL=sqlite:///data/bot.db
|
||||
|
||||
# 其他配置
|
||||
SESSION_TIMEOUT=300
|
||||
CONCURRENCY_LIMIT=5
|
||||
34
config.py
34
config.py
@@ -9,11 +9,20 @@ load_dotenv()
|
||||
# 项目根目录
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
|
||||
# WPS Webhook配置
|
||||
# WPS Webhook配置 - 使用函数动态获取
|
||||
WEBHOOK_URL = os.getenv(
|
||||
"WEBHOOK_URL",
|
||||
"https://xz.wps.cn/api/v1/webhook/send?key=da86927e491f2aef4b964223687c2c80"
|
||||
)
|
||||
"WEBHOOK_URL",
|
||||
"https://xz.wps.cn/api/v1/webhook/send?key=da86927e491f2aef4b964223687c2c80"
|
||||
)
|
||||
|
||||
def SetWebhookURL(url: str):
|
||||
"""设置Webhook URL"""
|
||||
global WEBHOOK_URL
|
||||
WEBHOOK_URL = url
|
||||
|
||||
def GetWebhookURL() -> str:
|
||||
"""获取Webhook URL"""
|
||||
return WEBHOOK_URL
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_PATH = os.getenv("DATABASE_PATH", str(BASE_DIR / "data" / "bot.db"))
|
||||
@@ -58,5 +67,18 @@ GAME_CONFIG = {
|
||||
"quiz": {
|
||||
"timeout": 60, # 答题超时时间(秒)
|
||||
},
|
||||
}
|
||||
|
||||
"idiom": {
|
||||
"max_history_display": 10, # 状态显示最近N个成语
|
||||
"starter_idioms": [ # 起始成语池
|
||||
"一马当先", "龙马精神", "马到成功", "开门见山",
|
||||
"心想事成", "万事如意", "风调雨顺", "国泰民安",
|
||||
"四季平安", "安居乐业", "业精于勤", "勤学苦练",
|
||||
"练达老成", "成竹在胸", "胸有成竹", "竹报平安",
|
||||
"平步青云", "云程发轫", "刃迎缕解", "解甲归田"
|
||||
]
|
||||
},
|
||||
"gomoku": {
|
||||
"max_concurrent_games": 5, # 每个聊天最多同时进行的游戏数
|
||||
"board_size": 15, # 棋盘大小
|
||||
},
|
||||
}
|
||||
1369
core/database.py
1369
core/database.py
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
"""数据模型定义"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
|
||||
class CallbackRequest(BaseModel):
|
||||
@@ -76,3 +76,20 @@ class QuizGameState(GameState):
|
||||
attempts: int = Field(0, description="尝试次数")
|
||||
max_attempts: int = Field(3, description="最大尝试次数")
|
||||
|
||||
|
||||
class PrivateMessageRequest(BaseModel):
|
||||
"""私聊消息请求模型"""
|
||||
user_id: int = Field(..., description="目标用户ID")
|
||||
content: str = Field(..., description="消息内容")
|
||||
msg_type: str = Field(default="text", description="消息类型: text 或 markdown")
|
||||
|
||||
|
||||
class CheckBatchRequest(BaseModel):
|
||||
"""批量检查请求模型"""
|
||||
user_ids: List[int] = Field(..., description="用户ID列表")
|
||||
|
||||
|
||||
class CheckBatchResponse(BaseModel):
|
||||
"""批量检查响应模型"""
|
||||
results: Dict[int, bool] = Field(..., description="用户ID到是否有URL的映射")
|
||||
|
||||
|
||||
6
data/ai_config.json
Normal file
6
data/ai_config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"host": "0.0.0.0",
|
||||
"port": 11434,
|
||||
"model": "qwen3:0.6b"
|
||||
}
|
||||
|
||||
BIN
data/bot.db
BIN
data/bot.db
Binary file not shown.
@@ -1,146 +1,147 @@
|
||||
{
|
||||
"fortunes": [
|
||||
{
|
||||
"level": "大吉",
|
||||
"color": "#FF4757",
|
||||
"emoji": "🌟",
|
||||
"description": "今天运势爆棚!做什么都顺利,是实现愿望的好日子!",
|
||||
"advice": "抓住机会,勇敢行动!"
|
||||
},
|
||||
{
|
||||
"level": "吉",
|
||||
"color": "#FF6348",
|
||||
"emoji": "✨",
|
||||
"description": "运势不错,事情会朝着好的方向发展。",
|
||||
"advice": "保持积极心态,好运自然来。"
|
||||
},
|
||||
{
|
||||
"level": "中吉",
|
||||
"color": "#FFA502",
|
||||
"emoji": "🍀",
|
||||
"description": "平稳的一天,虽无大喜但也无大忧。",
|
||||
"advice": "脚踏实地,稳中求进。"
|
||||
},
|
||||
{
|
||||
"level": "小吉",
|
||||
"color": "#F79F1F",
|
||||
"emoji": "🌈",
|
||||
"description": "有一些小确幸会出现,注意把握。",
|
||||
"advice": "留心身边的小美好。"
|
||||
},
|
||||
{
|
||||
"level": "平",
|
||||
"color": "#A3A3A3",
|
||||
"emoji": "☁️",
|
||||
"description": "平淡的一天,没有特别的起伏。",
|
||||
"advice": "平常心对待,顺其自然。"
|
||||
},
|
||||
{
|
||||
"level": "小凶",
|
||||
"color": "#747D8C",
|
||||
"emoji": "🌧️",
|
||||
"description": "可能会遇到一些小困难,需要谨慎应对。",
|
||||
"advice": "小心行事,三思而后行。"
|
||||
},
|
||||
{
|
||||
"level": "凶",
|
||||
"color": "#57606F",
|
||||
"emoji": "⚡",
|
||||
"description": "今天不太顺利,建议低调行事。",
|
||||
"advice": "韬光养晦,静待时机。"
|
||||
}
|
||||
],
|
||||
"tarot": [
|
||||
{
|
||||
"name": "愚者",
|
||||
"emoji": "🃏",
|
||||
"meaning": "新的开始、冒险、天真",
|
||||
"advice": "勇敢踏出第一步,迎接新的旅程。"
|
||||
},
|
||||
{
|
||||
"name": "魔术师",
|
||||
"emoji": "🎩",
|
||||
"meaning": "创造力、技能、意志力",
|
||||
"advice": "发挥你的才能,创造属于自己的奇迹。"
|
||||
},
|
||||
{
|
||||
"name": "女祭司",
|
||||
"emoji": "🔮",
|
||||
"meaning": "直觉、神秘、内在智慧",
|
||||
"advice": "倾听内心的声音,答案就在你心中。"
|
||||
},
|
||||
{
|
||||
"name": "皇后",
|
||||
"emoji": "👑",
|
||||
"meaning": "丰盛、养育、美好",
|
||||
"advice": "享受生活的美好,善待自己和他人。"
|
||||
},
|
||||
{
|
||||
"name": "皇帝",
|
||||
"emoji": "⚔️",
|
||||
"meaning": "权威、秩序、掌控",
|
||||
"advice": "建立规则,掌控局面。"
|
||||
},
|
||||
{
|
||||
"name": "恋人",
|
||||
"emoji": "💕",
|
||||
"meaning": "爱情、选择、和谐",
|
||||
"advice": "跟随你的心,做出正确的选择。"
|
||||
},
|
||||
{
|
||||
"name": "战车",
|
||||
"emoji": "🏎️",
|
||||
"meaning": "胜利、决心、前进",
|
||||
"advice": "坚定信念,勇往直前。"
|
||||
},
|
||||
{
|
||||
"name": "力量",
|
||||
"emoji": "💪",
|
||||
"meaning": "勇气、耐心、内在力量",
|
||||
"advice": "发掘内在的力量,温柔而坚定。"
|
||||
},
|
||||
{
|
||||
"name": "隐士",
|
||||
"emoji": "🕯️",
|
||||
"meaning": "内省、寻找、孤独",
|
||||
"advice": "静下心来,寻找内心的答案。"
|
||||
},
|
||||
{
|
||||
"name": "命运之轮",
|
||||
"emoji": "🎡",
|
||||
"meaning": "转变、命运、循环",
|
||||
"advice": "接受变化,一切都在轮转中。"
|
||||
},
|
||||
{
|
||||
"name": "正义",
|
||||
"emoji": "⚖️",
|
||||
"meaning": "公平、真相、因果",
|
||||
"advice": "坚持正义,真相终会大白。"
|
||||
},
|
||||
{
|
||||
"name": "星星",
|
||||
"emoji": "⭐",
|
||||
"meaning": "希望、灵感、宁静",
|
||||
"advice": "保持希望,光明就在前方。"
|
||||
},
|
||||
{
|
||||
"name": "月亮",
|
||||
"emoji": "🌙",
|
||||
"meaning": "潜意识、幻想、不确定",
|
||||
"advice": "信任直觉,但要分辨幻想与现实。"
|
||||
},
|
||||
{
|
||||
"name": "太阳",
|
||||
"emoji": "☀️",
|
||||
"meaning": "快乐、成功、活力",
|
||||
"advice": "享受阳光,分享你的快乐。"
|
||||
},
|
||||
{
|
||||
"name": "世界",
|
||||
"emoji": "🌍",
|
||||
"meaning": "完成、成就、圆满",
|
||||
"advice": "庆祝你的成就,准备迎接新的循环。"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"fortunes": [
|
||||
{
|
||||
"level": "大吉",
|
||||
"color": "#FF4757",
|
||||
"emoji": "🌟",
|
||||
"description": "今天运势爆棚!做什么都顺利,是实现愿望的好日子!",
|
||||
"advice": "抓住机会,勇敢行动!"
|
||||
},
|
||||
{
|
||||
"level": "吉",
|
||||
"color": "#FF6348",
|
||||
"emoji": "✨",
|
||||
"description": "运势不错,事情会朝着好的方向发展。",
|
||||
"advice": "保持积极心态,好运自然来。"
|
||||
},
|
||||
{
|
||||
"level": "中吉",
|
||||
"color": "#FFA502",
|
||||
"emoji": "🍀",
|
||||
"description": "平稳的一天,虽无大喜但也无大忧。",
|
||||
"advice": "脚踏实地,稳中求进。"
|
||||
},
|
||||
{
|
||||
"level": "小吉",
|
||||
"color": "#F79F1F",
|
||||
"emoji": "🌈",
|
||||
"description": "有一些小确幸会出现,注意把握。",
|
||||
"advice": "留心身边的小美好。"
|
||||
},
|
||||
{
|
||||
"level": "平",
|
||||
"color": "#A3A3A3",
|
||||
"emoji": "☁️",
|
||||
"description": "平淡的一天,没有特别的起伏。",
|
||||
"advice": "平常心对待,顺其自然。"
|
||||
},
|
||||
{
|
||||
"level": "小凶",
|
||||
"color": "#747D8C",
|
||||
"emoji": "🌧️",
|
||||
"description": "可能会遇到一些小困难,需要谨慎应对。",
|
||||
"advice": "小心行事,三思而后行。"
|
||||
},
|
||||
{
|
||||
"level": "凶",
|
||||
"color": "#57606F",
|
||||
"emoji": "⚡",
|
||||
"description": "今天不太顺利,建议低调行事。",
|
||||
"advice": "韬光养晦,静待时机。"
|
||||
}
|
||||
],
|
||||
"tarot": [
|
||||
{
|
||||
"name": "愚者",
|
||||
"emoji": "🃏",
|
||||
"meaning": "新的开始、冒险、天真",
|
||||
"advice": "勇敢踏出第一步,迎接新的旅程。"
|
||||
},
|
||||
{
|
||||
"name": "魔术师",
|
||||
"emoji": "🎩",
|
||||
"meaning": "创造力、技能、意志力",
|
||||
"advice": "发挥你的才能,创造属于自己的奇迹。"
|
||||
},
|
||||
{
|
||||
"name": "女祭司",
|
||||
"emoji": "🔮",
|
||||
"meaning": "直觉、神秘、内在智慧",
|
||||
"advice": "倾听内心的声音,答案就在你心中。"
|
||||
},
|
||||
{
|
||||
"name": "皇后",
|
||||
"emoji": "👑",
|
||||
"meaning": "丰盛、养育、美好",
|
||||
"advice": "享受生活的美好,善待自己和他人。"
|
||||
},
|
||||
{
|
||||
"name": "皇帝",
|
||||
"emoji": "⚔️",
|
||||
"meaning": "权威、秩序、掌控",
|
||||
"advice": "建立规则,掌控局面。"
|
||||
},
|
||||
{
|
||||
"name": "恋人",
|
||||
"emoji": "💕",
|
||||
"meaning": "爱情、选择、和谐",
|
||||
"advice": "跟随你的心,做出正确的选择。"
|
||||
},
|
||||
{
|
||||
"name": "战车",
|
||||
"emoji": "🏎️",
|
||||
"meaning": "胜利、决心、前进",
|
||||
"advice": "坚定信念,勇往直前。"
|
||||
},
|
||||
{
|
||||
"name": "力量",
|
||||
"emoji": "💪",
|
||||
"meaning": "勇气、耐心、内在力量",
|
||||
"advice": "发掘内在的力量,温柔而坚定。"
|
||||
},
|
||||
{
|
||||
"name": "隐士",
|
||||
"emoji": "🕯️",
|
||||
"meaning": "内省、寻找、孤独",
|
||||
"advice": "静下心来,寻找内心的答案。"
|
||||
},
|
||||
{
|
||||
"name": "命运之轮",
|
||||
"emoji": "🎡",
|
||||
"meaning": "转变、命运、循环",
|
||||
"advice": "接受变化,一切都在轮转中。"
|
||||
},
|
||||
{
|
||||
"name": "正义",
|
||||
"emoji": "⚖️",
|
||||
"meaning": "公平、真相、因果",
|
||||
"advice": "坚持正义,真相终会大白。"
|
||||
},
|
||||
{
|
||||
"name": "星星",
|
||||
"emoji": "⭐",
|
||||
"meaning": "希望、灵感、宁静",
|
||||
"advice": "保持希望,光明就在前方。"
|
||||
},
|
||||
{
|
||||
"name": "月亮",
|
||||
"emoji": "🌙",
|
||||
"meaning": "潜意识、幻想、不确定",
|
||||
"advice": "信任直觉,但要分辨幻想与现实。"
|
||||
},
|
||||
{
|
||||
"name": "太阳",
|
||||
"emoji": "☀️",
|
||||
"meaning": "快乐、成功、活力",
|
||||
"advice": "享受阳光,分享你的快乐。"
|
||||
},
|
||||
{
|
||||
"name": "世界",
|
||||
"emoji": "🌍",
|
||||
"meaning": "完成、成就、圆满",
|
||||
"advice": "庆祝你的成就,准备迎接新的循环。"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
6
data/idiom_blacklist.json
Normal file
6
data/idiom_blacklist.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"blacklist": [],
|
||||
"description": "成语接龙游戏全局黑名单,被拒绝的词语将永久不可使用"
|
||||
}
|
||||
|
||||
|
||||
249
data/quiz.json
249
data/quiz.json
@@ -1,125 +1,126 @@
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"id": 1,
|
||||
"question": "Python之父是谁?",
|
||||
"answer": "Guido van Rossum",
|
||||
"keywords": ["Guido", "吉多", "van Rossum"],
|
||||
"hint": "荷兰程序员,创建了Python语言",
|
||||
"category": "编程"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"question": "世界上最高的山峰是什么?",
|
||||
"answer": "珠穆朗玛峰",
|
||||
"keywords": ["珠穆朗玛", "珠峰", "Everest", "Mt. Everest"],
|
||||
"hint": "位于喜马拉雅山脉",
|
||||
"category": "地理"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"question": "一年有多少天?",
|
||||
"answer": "365",
|
||||
"keywords": ["365", "三百六十五"],
|
||||
"hint": "平年的天数",
|
||||
"category": "常识"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"question": "中国的首都是哪个城市?",
|
||||
"answer": "北京",
|
||||
"keywords": ["北京", "Beijing"],
|
||||
"hint": "位于华北地区",
|
||||
"category": "地理"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"question": "光速是多少?",
|
||||
"answer": "300000",
|
||||
"keywords": ["300000", "30万", "3*10^8", "3e8"],
|
||||
"hint": "单位:千米/秒,约30万",
|
||||
"category": "物理"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"question": "世界上最大的海洋是什么?",
|
||||
"answer": "太平洋",
|
||||
"keywords": ["太平洋", "Pacific"],
|
||||
"hint": "占地球表面积约46%",
|
||||
"category": "地理"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"question": "一个字节(Byte)等于多少位(bit)?",
|
||||
"answer": "8",
|
||||
"keywords": ["8", "八", "8bit"],
|
||||
"hint": "计算机基础知识",
|
||||
"category": "计算机"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"question": "人类的正常体温约是多少摄氏度?",
|
||||
"answer": "37",
|
||||
"keywords": ["37", "三十七", "36.5", "37度"],
|
||||
"hint": "36-37度之间",
|
||||
"category": "生物"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"question": "HTTP协议默认使用哪个端口?",
|
||||
"answer": "80",
|
||||
"keywords": ["80", "八十"],
|
||||
"hint": "HTTPS使用443",
|
||||
"category": "计算机"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"question": "一个小时有多少分钟?",
|
||||
"answer": "60",
|
||||
"keywords": ["60", "六十"],
|
||||
"hint": "基础时间单位",
|
||||
"category": "常识"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"question": "太阳系中最大的行星是什么?",
|
||||
"answer": "木星",
|
||||
"keywords": ["木星", "Jupiter"],
|
||||
"hint": "体积和质量都是最大的",
|
||||
"category": "天文"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"question": "二进制中10等于十进制的多少?",
|
||||
"answer": "2",
|
||||
"keywords": ["2", "二", "两"],
|
||||
"hint": "1*2^1 + 0*2^0",
|
||||
"category": "数学"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"question": "中国有多少个省级行政区?",
|
||||
"answer": "34",
|
||||
"keywords": ["34", "三十四"],
|
||||
"hint": "23个省+5个自治区+4个直辖市+2个特别行政区",
|
||||
"category": "地理"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"question": "圆周率π约等于多少?(保留两位小数)",
|
||||
"answer": "3.14",
|
||||
"keywords": ["3.14", "3.1415", "3.14159"],
|
||||
"hint": "圆的周长与直径的比值",
|
||||
"category": "数学"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"question": "世界上使用人数最多的语言是什么?",
|
||||
"answer": "中文",
|
||||
"keywords": ["中文", "汉语", "Chinese", "普通话"],
|
||||
"hint": "中国的官方语言",
|
||||
"category": "语言"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"questions": [
|
||||
{
|
||||
"id": 1,
|
||||
"question": "Python之父是谁?",
|
||||
"answer": "Guido van Rossum",
|
||||
"keywords": ["Guido", "吉多", "van Rossum"],
|
||||
"hint": "荷兰程序员,创建了Python语言",
|
||||
"category": "编程"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"question": "世界上最高的山峰是什么?",
|
||||
"answer": "珠穆朗玛峰",
|
||||
"keywords": ["珠穆朗玛", "珠峰", "Everest", "Mt. Everest"],
|
||||
"hint": "位于喜马拉雅山脉",
|
||||
"category": "地理"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"question": "一年有多少天?",
|
||||
"answer": "365",
|
||||
"keywords": ["365", "三百六十五"],
|
||||
"hint": "平年的天数",
|
||||
"category": "常识"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"question": "中国的首都是哪个城市?",
|
||||
"answer": "北京",
|
||||
"keywords": ["北京", "Beijing"],
|
||||
"hint": "位于华北地区",
|
||||
"category": "地理"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"question": "光速是多少?",
|
||||
"answer": "300000",
|
||||
"keywords": ["300000", "30万", "3*10^8", "3e8"],
|
||||
"hint": "单位:千米/秒,约30万",
|
||||
"category": "物理"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"question": "世界上最大的海洋是什么?",
|
||||
"answer": "太平洋",
|
||||
"keywords": ["太平洋", "Pacific"],
|
||||
"hint": "占地球表面积约46%",
|
||||
"category": "地理"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"question": "一个字节(Byte)等于多少位(bit)?",
|
||||
"answer": "8",
|
||||
"keywords": ["8", "八", "8bit"],
|
||||
"hint": "计算机基础知识",
|
||||
"category": "计算机"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"question": "人类的正常体温约是多少摄氏度?",
|
||||
"answer": "37",
|
||||
"keywords": ["37", "三十七", "36.5", "37度"],
|
||||
"hint": "36-37度之间",
|
||||
"category": "生物"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"question": "HTTP协议默认使用哪个端口?",
|
||||
"answer": "80",
|
||||
"keywords": ["80", "八十"],
|
||||
"hint": "HTTPS使用443",
|
||||
"category": "计算机"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"question": "一个小时有多少分钟?",
|
||||
"answer": "60",
|
||||
"keywords": ["60", "六十"],
|
||||
"hint": "基础时间单位",
|
||||
"category": "常识"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"question": "太阳系中最大的行星是什么?",
|
||||
"answer": "木星",
|
||||
"keywords": ["木星", "Jupiter"],
|
||||
"hint": "体积和质量都是最大的",
|
||||
"category": "天文"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"question": "二进制中10等于十进制的多少?",
|
||||
"answer": "2",
|
||||
"keywords": ["2", "二", "两"],
|
||||
"hint": "1*2^1 + 0*2^0",
|
||||
"category": "数学"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"question": "中国有多少个省级行政区?",
|
||||
"answer": "34",
|
||||
"keywords": ["34", "三十四"],
|
||||
"hint": "23个省+5个自治区+4个直辖市+2个特别行政区",
|
||||
"category": "地理"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"question": "圆周率π约等于多少?(保留两位小数)",
|
||||
"answer": "3.14",
|
||||
"keywords": ["3.14", "3.1415", "3.14159"],
|
||||
"hint": "圆的周长与直径的比值",
|
||||
"category": "数学"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"question": "世界上使用人数最多的语言是什么?",
|
||||
"answer": "中文",
|
||||
"keywords": ["中文", "汉语", "Chinese", "普通话"],
|
||||
"hint": "中国的官方语言",
|
||||
"category": "语言"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
75
diagnose_checkin.py
Normal file
75
diagnose_checkin.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""签到问题诊断脚本"""
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
def diagnose_checkin_issue():
|
||||
"""诊断签到问题"""
|
||||
print("=== 签到问题诊断 ===")
|
||||
|
||||
# 1. 检查数据库文件
|
||||
db_path = "data/bot.db"
|
||||
print(f"1. 数据库文件路径: {db_path}")
|
||||
print(f" 文件存在: {os.path.exists(db_path)}")
|
||||
|
||||
if os.path.exists(db_path):
|
||||
stat = os.stat(db_path)
|
||||
print(f" 文件大小: {stat.st_size} bytes")
|
||||
print(f" 文件权限: {oct(stat.st_mode)}")
|
||||
print(f" 最后修改: {datetime.fromtimestamp(stat.st_mtime)}")
|
||||
|
||||
# 2. 检查数据库目录
|
||||
db_dir = Path(db_path).parent
|
||||
print(f"2. 数据库目录: {db_dir}")
|
||||
print(f" 目录存在: {db_dir.exists()}")
|
||||
print(f" 目录可写: {os.access(db_dir, os.W_OK)}")
|
||||
|
||||
# 3. 测试数据库连接
|
||||
print("3. 测试数据库连接...")
|
||||
try:
|
||||
conn = sqlite3.connect(db_path, timeout=10.0)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = cursor.fetchall()
|
||||
print(f" 数据库表: {[table[0] for table in tables]}")
|
||||
|
||||
# 检查用户积分表
|
||||
if ('user_points',) in tables:
|
||||
cursor.execute("SELECT COUNT(*) FROM user_points")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" 用户积分记录数: {count}")
|
||||
|
||||
if count > 0:
|
||||
cursor.execute("SELECT user_id, available_points FROM user_points LIMIT 3")
|
||||
users = cursor.fetchall()
|
||||
print(f" 示例用户积分: {users}")
|
||||
|
||||
conn.close()
|
||||
print(" 数据库连接: 成功")
|
||||
except Exception as e:
|
||||
print(f" 数据库连接: 失败 - {e}")
|
||||
|
||||
# 4. 检查磁盘空间
|
||||
print("4. 检查磁盘空间...")
|
||||
try:
|
||||
statvfs = os.statvfs(db_dir)
|
||||
free_space = statvfs.f_frsize * statvfs.f_bavail
|
||||
total_space = statvfs.f_frsize * statvfs.f_blocks
|
||||
print(f" 可用空间: {free_space / (1024*1024):.1f} MB")
|
||||
print(f" 总空间: {total_space / (1024*1024):.1f} MB")
|
||||
except Exception as e:
|
||||
print(f" 磁盘空间检查失败: {e}")
|
||||
|
||||
# 5. 检查进程权限
|
||||
print("5. 检查进程权限...")
|
||||
print(f" 当前用户: {os.getuid() if hasattr(os, 'getuid') else 'N/A'}")
|
||||
print(f" 当前组: {os.getgid() if hasattr(os, 'getgid') else 'N/A'}")
|
||||
|
||||
print("=== 诊断完成 ===")
|
||||
|
||||
if __name__ == "__main__":
|
||||
diagnose_checkin_issue()
|
||||
365
games/adventure.py
Normal file
365
games/adventure.py
Normal file
@@ -0,0 +1,365 @@
|
||||
"""冒险系统游戏模块"""
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from games.base import BaseGame
|
||||
from utils.parser import CommandParser
|
||||
from core.database import get_db
|
||||
from typing import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdventureGame(BaseGame):
|
||||
"""冒险系统游戏"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化游戏"""
|
||||
super().__init__()
|
||||
self.db = get_db()
|
||||
|
||||
# 奖品池配置
|
||||
self.prize_pool: List[Tuple[int, float, str]] = [
|
||||
# (权重, 倍率, 描述)
|
||||
(300, 1, "少量积分"),
|
||||
(250, 2, "中等积分"),
|
||||
(200, 2, "大量积分"),
|
||||
(150, 5, "丰厚积分"),
|
||||
(100, 10, "丰厚积分"),
|
||||
(50, 100, "🌟 巨额积分"),
|
||||
(10, 1000, "💎 传说积分"),
|
||||
]
|
||||
|
||||
self.total_weight: int = 0
|
||||
for weight,_,_ in self.prize_pool:
|
||||
self.total_weight += weight
|
||||
|
||||
def _parse_time_string(self, time_str: str) -> int:
|
||||
"""解析时间字符串,支持 h/m/s 格式
|
||||
|
||||
支持的格式示例:
|
||||
- "1h30m10s" -> 5410秒
|
||||
- "30m" -> 1800秒
|
||||
- "10s" -> 10秒
|
||||
- "1h30m" -> 5400秒
|
||||
- "3600" -> 3600秒(纯数字,按秒处理)
|
||||
|
||||
Args:
|
||||
time_str: 时间字符串
|
||||
|
||||
Returns:
|
||||
解析后的秒数,如果解析失败返回None
|
||||
"""
|
||||
if not time_str:
|
||||
return None
|
||||
|
||||
# 如果是纯数字,直接返回
|
||||
if time_str.isdigit():
|
||||
return int(time_str)
|
||||
|
||||
# 使用正则表达式匹配 h/m/s 格式,确保整个字符串匹配
|
||||
pattern = r'^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$'
|
||||
match = re.match(pattern, time_str.lower())
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
hours = int(match.group(1) or 0)
|
||||
minutes = int(match.group(2) or 0)
|
||||
seconds = int(match.group(3) or 0)
|
||||
|
||||
# 如果所有值都是0,返回None
|
||||
if hours == 0 and minutes == 0 and seconds == 0:
|
||||
return None
|
||||
|
||||
total_seconds = hours * 3600 + minutes * 60 + seconds
|
||||
return total_seconds
|
||||
|
||||
def _format_time(self, seconds: int) -> str:
|
||||
"""将秒数格式化为 "X时X分X秒" 格式
|
||||
|
||||
Args:
|
||||
seconds: 秒数
|
||||
|
||||
Returns:
|
||||
格式化的时间字符串,如 "1时30分10秒"、"30分10秒"、"10秒"
|
||||
"""
|
||||
if seconds < 0:
|
||||
seconds = 0
|
||||
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
secs = seconds % 60
|
||||
|
||||
parts = []
|
||||
if hours > 0:
|
||||
parts.append(f"{hours}时")
|
||||
if minutes > 0:
|
||||
parts.append(f"{minutes}分")
|
||||
if secs > 0 or not parts:
|
||||
parts.append(f"{secs}秒")
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理冒险相关指令
|
||||
|
||||
Args:
|
||||
command: 指令,如 ".adventure", ".adventure 10", ".adventure stats"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
回复消息
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip().lower()
|
||||
|
||||
|
||||
# 冒险说明
|
||||
if args in ['help', '帮助', 'info']:
|
||||
return self._get_adventure_help()
|
||||
|
||||
# 放弃当前冒险(按最低倍率结算已冒险时间)
|
||||
if args in ['abandon', '放弃']:
|
||||
return await self._abandon_adventure(chat_id, user_id)
|
||||
|
||||
# 默认:冒险耗时1秒
|
||||
else:
|
||||
# 解析消耗时间
|
||||
cost_time = 1 # 默认消耗1秒
|
||||
if args:
|
||||
parsed_time = self._parse_time_string(args)
|
||||
if parsed_time is not None:
|
||||
cost_time = parsed_time
|
||||
else:
|
||||
return f"❌ 时间格式错误!请使用以下格式:\n- 纯数字(秒):`.adventure 60`\n- 时分秒格式:`.adventure 1h30m10s`\n- 分钟秒格式:`.adventure 30m10s`\n- 只有秒:`.adventure 10s`"
|
||||
|
||||
return await self._perform_adventure(chat_id, user_id, cost_time)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理冒险指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
async def _perform_adventure(self, chat_id: int, user_id: int, cost_time: int) -> str:
|
||||
"""执行冒险耗时
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID(使用0作为用户级标识)
|
||||
user_id: 用户ID
|
||||
cost_time: 消耗时间(秒)
|
||||
|
||||
Returns:
|
||||
抽奖结果消息
|
||||
"""
|
||||
# 参数验证
|
||||
if cost_time < 1:
|
||||
return "❌ 冒险时间至少需要1秒!"
|
||||
|
||||
# 查询冒险状态(使用chat_id=0表示用户级状态)
|
||||
state = self.db.get_game_state(0, user_id, 'adventure')
|
||||
current_time = int(time.time())
|
||||
|
||||
# 情况1:用户已有未完成的冒险
|
||||
if state:
|
||||
try:
|
||||
state_data = state['state_data']
|
||||
start_time = state_data.get('start_time', 0)
|
||||
saved_cost_time = state_data.get('cost_time', 1)
|
||||
end_time = start_time + saved_cost_time
|
||||
remaining_seconds = end_time - current_time
|
||||
|
||||
# 情况1.1:冒险已完成(时间已到或过期)
|
||||
if remaining_seconds <= 0:
|
||||
# 选择奖品池
|
||||
prize_pool = self.prize_pool
|
||||
|
||||
# 执行抽奖
|
||||
reward = self._draw_prize(prize_pool)
|
||||
reward_points = int(reward['value'] * saved_cost_time)
|
||||
|
||||
# 处理奖励
|
||||
self.db.add_points(user_id, reward_points, "adventure", f"冒险奖励")
|
||||
|
||||
# 删除冒险状态
|
||||
self.db.delete_game_state(0, user_id, 'adventure')
|
||||
|
||||
# 获取更新后的积分信息
|
||||
updated_points = self.db.get_user_points(user_id)
|
||||
|
||||
# 格式化输出
|
||||
time_str = self._format_time(saved_cost_time)
|
||||
text = f"## ⚡️ 冒险结果\n\n"
|
||||
text += f"**消耗时间**: {time_str}\n\n"
|
||||
text += f"**{reward['description']}**: 获得 {reward_points} 积分\n\n"
|
||||
text += f"**当前积分**: {updated_points['points']} 分\n\n"
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:冒险进行时不能炼金!"
|
||||
|
||||
return text
|
||||
|
||||
# 情况1.2:冒险未完成,返回等待提示
|
||||
wait_msg = self._format_time(remaining_seconds)
|
||||
saved_time_str = self._format_time(saved_cost_time)
|
||||
|
||||
text = f"## ⚡️ 冒险进行中\n\n"
|
||||
text += f"你正在进行一次冒险,还需等待 **{wait_msg}** 才能完成。\n\n"
|
||||
text += f"**当前冒险时长**: {saved_time_str}\n\n"
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:冒险期间无法进行炼金,请耐心等待!"
|
||||
|
||||
return text
|
||||
|
||||
except Exception as e:
|
||||
# 状态数据异常,删除损坏状态并允许重新开始
|
||||
logger.error(f"冒险状态数据异常: {e}", exc_info=True)
|
||||
self.db.delete_game_state(0, user_id, 'adventure')
|
||||
# 继续执行到情况3(开始新冒险)
|
||||
|
||||
# 情况3:用户没有冒险状态,开始新的冒险
|
||||
state_data = {
|
||||
'start_time': current_time,
|
||||
'cost_time': cost_time
|
||||
}
|
||||
|
||||
# 保存冒险状态
|
||||
self.db.save_game_state(0, user_id, 'adventure', state_data)
|
||||
|
||||
# 计算预计完成时间
|
||||
end_time = current_time + cost_time
|
||||
end_datetime = datetime.fromtimestamp(end_time)
|
||||
current_datetime = datetime.fromtimestamp(current_time)
|
||||
# 判断是否跨天:如果完成日期和当前日期不同,或跨年,则显示完整日期时间
|
||||
if (end_datetime.date() != current_datetime.date() or
|
||||
end_datetime.year != current_datetime.year):
|
||||
end_time_str = end_datetime.strftime('%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
end_time_str = end_datetime.strftime('%H:%M:%S')
|
||||
cost_time_str = self._format_time(cost_time)
|
||||
|
||||
text = f"## ⚡️ 冒险开始\n\n"
|
||||
text += f"你已经开始了冒险之旅,本次冒险将持续 **{cost_time_str}**。\n\n"
|
||||
text += f"**预计完成时间**: {end_time_str}\n\n"
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:冒险期间无法进行炼金,完成后使用 `.adventure` 获取奖励!"
|
||||
|
||||
return text
|
||||
|
||||
async def _abandon_adventure(self, chat_id: int, user_id: int) -> str:
|
||||
"""放弃当前冒险,按最低倍率结算已冒险时间
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID(使用0作为用户级标识)
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
放弃结果消息
|
||||
"""
|
||||
try:
|
||||
# 查询冒险状态
|
||||
state = self.db.get_game_state(0, user_id, 'adventure')
|
||||
if not state:
|
||||
return "❌ 当前没有进行中的冒险,可使用 `.adventure` 开始新的冒险。"
|
||||
|
||||
state_data = state.get('state_data', {})
|
||||
start_time = state_data.get('start_time')
|
||||
cost_time = state_data.get('cost_time')
|
||||
if start_time is None or cost_time is None:
|
||||
# 状态异常,清理并提示
|
||||
self.db.delete_game_state(0, user_id, 'adventure')
|
||||
return "⚠️ 冒险状态异常已清理,请使用 `.adventure` 重新开始。"
|
||||
|
||||
current_time = int(time.time())
|
||||
elapsed_seconds = max(0, current_time - int(start_time))
|
||||
if elapsed_seconds < 1:
|
||||
elapsed_seconds = 1
|
||||
|
||||
# 计算最低倍率
|
||||
try:
|
||||
min_multiplier = min(m for _, m, _ in self.prize_pool)
|
||||
except Exception:
|
||||
# 兜底:若奖池异常,按0.5处理
|
||||
min_multiplier = 0.5
|
||||
|
||||
reward_points = int(min_multiplier * elapsed_seconds)
|
||||
if reward_points < 0:
|
||||
reward_points = 0
|
||||
|
||||
# 发放奖励并清理状态
|
||||
if reward_points > 0:
|
||||
self.db.add_points(user_id, reward_points, "adventure", "冒险放弃奖励")
|
||||
self.db.delete_game_state(0, user_id, 'adventure')
|
||||
|
||||
# 查询当前积分
|
||||
updated_points = self.db.get_user_points(user_id)
|
||||
|
||||
# 输出
|
||||
elapsed_time_str = self._format_time(elapsed_seconds)
|
||||
text = f"## ⚡️ 冒险放弃\n\n"
|
||||
text += f"**已计入时间**: {elapsed_time_str}\n\n"
|
||||
text += f"**最低倍率**: {min_multiplier} 倍\n\n"
|
||||
text += f"**获得积分**: {reward_points} 分\n\n"
|
||||
text += f"**当前积分**: {updated_points['points']} 分\n\n"
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:可随时使用 `.adventure` 再次踏上冒险之旅!"
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.error(f"放弃冒险时出错: {e}", exc_info=True)
|
||||
# 失败时不影响原状态,返回提示
|
||||
return f"❌ 放弃冒险失败:{str(e)}"
|
||||
|
||||
def _draw_prize(self, prize_pool: list) -> dict:
|
||||
"""从奖品池中抽取奖品
|
||||
|
||||
Args:
|
||||
prize_pool: 奖品池,格式为 (权重, 倍率, 描述)
|
||||
|
||||
Returns:
|
||||
奖品信息,格式为 {'value': 倍率, 'description': 描述}
|
||||
"""
|
||||
# 生成随机数
|
||||
rand = random.random()*self.total_weight
|
||||
cumulative_prob = 0.0
|
||||
|
||||
for weight, multiplier, description in prize_pool:
|
||||
cumulative_prob += weight
|
||||
if rand <= cumulative_prob:
|
||||
return {
|
||||
'value': multiplier,
|
||||
'description': description
|
||||
}
|
||||
|
||||
# 兜底返回第一个奖品
|
||||
return {
|
||||
'value': prize_pool[0][1],
|
||||
'description': prize_pool[0][2]
|
||||
}
|
||||
|
||||
def _get_adventure_help(self) -> str:
|
||||
"""获取冒险帮助信息
|
||||
|
||||
Returns:
|
||||
帮助信息消息
|
||||
"""
|
||||
text = f"## ⚡️ 冒险系统\n\n"
|
||||
text += f"### 基础用法\n"
|
||||
text += f"- `.adventure` - 消耗1秒进行冒险\n"
|
||||
text += f"- `.adventure 60` - 消耗60秒进行冒险\n"
|
||||
text += f"- `.adventure 1h30m10s` - 消耗1小时30分10秒进行冒险\n"
|
||||
text += f"- `.adventure 30m` - 消耗30分钟进行冒险\n"
|
||||
text += f"- `.adventure 10s` - 消耗10秒进行冒险\n\n"
|
||||
text += f"**时间格式说明**:支持时分秒组合,如 `1h30m10s`、`30m`、`10s`,也支持纯数字(按秒计算)。\n\n"
|
||||
text += f"### 其他功能\n"
|
||||
text += f"- `.adventure abandon` - 放弃当前冒险,按最低倍率结算已冒险时间\n"
|
||||
text += f"- `.adventure 放弃` - 放弃当前冒险,按最低倍率结算已冒险时间\n"
|
||||
text += f"- `.adventure help` - 查看帮助\n\n"
|
||||
|
||||
return text
|
||||
|
||||
def get_help(self) -> str:
|
||||
"""获取帮助信息"""
|
||||
return self._get_adventure_help()
|
||||
530
games/ai_chat.py
Normal file
530
games/ai_chat.py
Normal file
@@ -0,0 +1,530 @@
|
||||
"""AI对话游戏模块"""
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
from games.base import BaseGame
|
||||
from utils.parser import CommandParser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 全局字典:存储每个chat_id的延迟任务句柄
|
||||
_pending_tasks: Dict[int, asyncio.Task] = {}
|
||||
|
||||
# 全局字典:存储每个chat_id的待处理消息队列
|
||||
_message_queues: Dict[int, List[Dict[str, Any]]] = {}
|
||||
|
||||
# 全局字典:存储每个chat_id的ChatEngine实例
|
||||
_chat_engines: Dict[int, Any] = {}
|
||||
|
||||
|
||||
class AIChatGame(BaseGame):
|
||||
"""AI对话游戏"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化游戏"""
|
||||
super().__init__()
|
||||
self.config_file = Path(__file__).parent.parent / "data" / "ai_config.json"
|
||||
self.wait_window = 10 # 固定10秒等待窗口
|
||||
|
||||
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理AI对话指令
|
||||
|
||||
Args:
|
||||
command: 指令,如 ".ai 问题" 或 ".aiconfig host=xxx port=xxx model=xxx"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
回复消息
|
||||
"""
|
||||
try:
|
||||
# 提取指令和参数
|
||||
cmd, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
# 判断是配置指令还是AI对话指令
|
||||
if cmd == '.aiconfig':
|
||||
return await self._handle_config(args, chat_id, user_id)
|
||||
else:
|
||||
# .ai 指令
|
||||
return await self._handle_ai(args, chat_id, user_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理AI对话指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
async def _handle_ai(self, content: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理AI对话请求
|
||||
|
||||
Args:
|
||||
content: 消息内容
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
回复消息
|
||||
"""
|
||||
# 如果内容为空,返回帮助信息
|
||||
if not content:
|
||||
return self.get_help()
|
||||
|
||||
# 将消息加入队列
|
||||
self._add_to_queue(chat_id, user_id, content)
|
||||
|
||||
# 取消旧的延迟任务(如果存在)
|
||||
if chat_id in _pending_tasks:
|
||||
old_task = _pending_tasks[chat_id]
|
||||
if not old_task.done():
|
||||
old_task.cancel()
|
||||
try:
|
||||
await old_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# 创建新的延迟任务
|
||||
task = asyncio.create_task(self._delayed_response(chat_id))
|
||||
_pending_tasks[chat_id] = task
|
||||
|
||||
# 不返回确认消息,静默处理
|
||||
return ""
|
||||
|
||||
async def _handle_config(self, args: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理配置请求
|
||||
|
||||
Args:
|
||||
args: 配置参数,格式如 "host=localhost port=11434 model=llama3.1"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
配置确认消息
|
||||
"""
|
||||
if not args:
|
||||
return "❌ 请提供配置参数\n\n格式:`.aiconfig host=xxx port=xxx model=xxx`\n\n示例:`.aiconfig host=localhost port=11434 model=llama3.1`"
|
||||
|
||||
# 解析配置参数
|
||||
config_updates = {}
|
||||
parts = args.split()
|
||||
for part in parts:
|
||||
if '=' in part:
|
||||
key, value = part.split('=', 1)
|
||||
key = key.strip().lower()
|
||||
value = value.strip()
|
||||
|
||||
if key == 'host':
|
||||
config_updates['host'] = value
|
||||
elif key == 'port':
|
||||
try:
|
||||
config_updates['port'] = int(value)
|
||||
except ValueError:
|
||||
return f"❌ 端口号必须是数字:{value}"
|
||||
elif key == 'model':
|
||||
config_updates['model'] = value
|
||||
else:
|
||||
return f"❌ 未知的配置项:{key}\n\n支持的配置项:host, port, model"
|
||||
|
||||
if not config_updates:
|
||||
return "❌ 未提供有效的配置参数"
|
||||
|
||||
# 加载现有配置
|
||||
current_config = self._load_config()
|
||||
|
||||
# 更新配置
|
||||
current_config.update(config_updates)
|
||||
|
||||
# 保存配置
|
||||
if self._save_config(current_config):
|
||||
# 清除所有ChatEngine缓存(配置变更需要重新创建)
|
||||
_chat_engines.clear()
|
||||
|
||||
return f"✅ 配置已更新\n\n**当前配置**:\n- 地址:{current_config['host']}\n- 端口:{current_config['port']}\n- 模型:{current_config['model']}"
|
||||
else:
|
||||
return "❌ 保存配置失败,请稍后重试"
|
||||
|
||||
def _add_to_queue(self, chat_id: int, user_id: int, content: str) -> None:
|
||||
"""将消息加入等待队列
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
content: 消息内容
|
||||
"""
|
||||
if chat_id not in _message_queues:
|
||||
_message_queues[chat_id] = []
|
||||
|
||||
_message_queues[chat_id].append({
|
||||
"user_id": user_id,
|
||||
"content": content,
|
||||
"timestamp": int(time.time())
|
||||
})
|
||||
|
||||
async def _delayed_response(self, chat_id: int) -> None:
|
||||
"""延迟回答任务
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
"""
|
||||
try:
|
||||
# 等待固定时间窗口
|
||||
await asyncio.sleep(self.wait_window)
|
||||
|
||||
# 检查队列中是否有消息
|
||||
if chat_id in _message_queues and _message_queues[chat_id]:
|
||||
# 生成回答
|
||||
response = await self._generate_response(chat_id)
|
||||
|
||||
# 清空队列
|
||||
_message_queues[chat_id] = []
|
||||
|
||||
# 发送回答
|
||||
if response:
|
||||
from utils.message import get_message_sender
|
||||
sender = get_message_sender()
|
||||
await sender.send_text(response)
|
||||
|
||||
# 从pending_tasks中移除任务句柄
|
||||
if chat_id in _pending_tasks:
|
||||
del _pending_tasks[chat_id]
|
||||
|
||||
except asyncio.CancelledError:
|
||||
# 任务被取消,正常情况,不需要记录错误
|
||||
logger.debug(f"延迟任务被取消: chat_id={chat_id}")
|
||||
if chat_id in _pending_tasks:
|
||||
del _pending_tasks[chat_id]
|
||||
except Exception as e:
|
||||
logger.error(f"延迟回答任务错误: {e}", exc_info=True)
|
||||
if chat_id in _pending_tasks:
|
||||
del _pending_tasks[chat_id]
|
||||
|
||||
async def _generate_response(self, chat_id: int) -> Optional[str]:
|
||||
"""使用LLM生成回答
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
|
||||
Returns:
|
||||
回答文本
|
||||
"""
|
||||
try:
|
||||
# 获取队列消息
|
||||
if chat_id not in _message_queues or not _message_queues[chat_id]:
|
||||
return None
|
||||
|
||||
messages = _message_queues[chat_id].copy()
|
||||
|
||||
# 获取ChatEngine实例
|
||||
chat_engine = self._get_chat_engine(chat_id)
|
||||
if not chat_engine:
|
||||
return "❌ AI服务初始化失败,请检查配置"
|
||||
|
||||
# 将消息按用户角色格式化并添加到ChatMemoryBuffer
|
||||
# 构建合并的消息内容(包含用户信息)
|
||||
merged_content = ""
|
||||
for msg in messages:
|
||||
user_id = msg['user_id']
|
||||
role = self._get_user_role(chat_id, user_id)
|
||||
merged_content += f"[{role}]: {msg['content']}\n"
|
||||
|
||||
# 去掉最后的换行
|
||||
merged_content = merged_content.strip()
|
||||
|
||||
# 调用ChatEngine生成回答
|
||||
# chat_engine是一个字典,包含llm, memory, system_prompt
|
||||
llm = chat_engine['llm']
|
||||
memory = chat_engine['memory']
|
||||
system_prompt = chat_engine['system_prompt']
|
||||
|
||||
# 构建完整的消息(包含系统提示和历史对话)
|
||||
full_message = f"{system_prompt}\n\n{merged_content}"
|
||||
|
||||
# 使用LLM生成回答(同步调用,在线程池中执行)
|
||||
response = await asyncio.to_thread(llm.complete, full_message)
|
||||
|
||||
# 返回回答文本
|
||||
return str(response)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"生成AI回答错误: {e}", exc_info=True)
|
||||
|
||||
# 获取当前配置以便在错误消息中显示
|
||||
try:
|
||||
config = self._load_config()
|
||||
config_info = f"\n\n当前配置:\n- 地址: {config['host']}\n- 端口: {config['port']}\n- 模型: {config['model']}"
|
||||
except:
|
||||
config_info = ""
|
||||
|
||||
# 提供更友好的错误消息
|
||||
if "Server disconnected" in error_msg or "RemoteProtocolError" in error_msg:
|
||||
config = self._load_config()
|
||||
test_cmd = f"curl -X POST http://{config.get('host', 'localhost')}:{config.get('port', 11434)}/api/generate -d '{{\"model\": \"{config.get('model', 'qwen3:0.6b')}\", \"prompt\": \"你是谁\", \"stream\": false}}'"
|
||||
return f"❌ AI服务连接失败,请检查:\n1. Ollama服务是否已启动(在笔记本上)\n2. NPS端口转发是否正常工作\n3. 配置的地址是否为服务器IP(不是localhost)\n4. 模型名称是否正确\n\n测试命令(在服务器上执行):\n{test_cmd}{config_info}\n\n使用 `.aiconfig` 命令检查和修改配置"
|
||||
elif "ConnectionError" in error_msg or "ConnectTimeout" in error_msg or "actively refused" in error_msg.lower() or "Empty reply" in error_msg:
|
||||
return f"❌ 无法连接到Ollama服务(连接被拒绝)\n\n根据NPS日志,笔记本上的Ollama服务拒绝了连接。\n\n**解决方案**:\n\n1. **检查Ollama是否运行**:\n 在笔记本上运行:`tasklist | findstr ollama` 或 `Get-Process | Where-Object {{$_.ProcessName -like '*ollama*'}}`\n\n2. **设置Ollama监听地址**(重要!):\n 在Windows上,Ollama默认只监听127.0.0.1,需要设置为0.0.0.0才能被NPS访问\n \n 方法A:设置环境变量(推荐)\n 1. 打开系统环境变量设置\n 2. 添加环境变量:`OLLAMA_HOST=0.0.0.0:11434`\n 3. 重启Ollama服务或重启电脑\n \n 方法B:在命令行启动时指定\n ```powershell\n $env:OLLAMA_HOST=\"0.0.0.0:11434\"\n ollama serve\n ```\n\n3. **检查Windows防火墙**:\n 确保允许11434端口的入站连接\n\n4. **验证监听地址**:\n 在笔记本上运行:`netstat -an | findstr 11434`\n 应该看到 `0.0.0.0:11434` 而不是 `127.0.0.1:11434`\n\n5. **测试本地连接**:\n 在笔记本上运行:`curl http://localhost:11434/api/tags`\n 应该返回模型列表\n\n提示:如果使用NPS转发,配置中的host应该是服务器的IP地址,不是localhost!{config_info}\n\n使用 `.aiconfig host=服务器IP` 修改配置"
|
||||
elif "timeout" in error_msg.lower():
|
||||
return f"❌ AI服务响应超时,请稍后重试{config_info}"
|
||||
else:
|
||||
return f"❌ 生成回答时出错: {error_msg}{config_info}"
|
||||
|
||||
def _get_chat_engine(self, chat_id: int) -> Any:
|
||||
"""获取或创建ChatEngine实例
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
|
||||
Returns:
|
||||
ChatEngine实例
|
||||
"""
|
||||
# 检查是否已存在
|
||||
if chat_id in _chat_engines:
|
||||
return _chat_engines[chat_id]
|
||||
|
||||
try:
|
||||
# 加载配置
|
||||
config = self._load_config()
|
||||
|
||||
# 导入llama_index模块
|
||||
from llama_index.llms.ollama import Ollama
|
||||
from llama_index.core.memory import ChatMemoryBuffer
|
||||
from llama_index.core import ChatPromptTemplate, Settings
|
||||
|
||||
# 创建Ollama LLM实例
|
||||
# 添加超时设置以避免长时间等待
|
||||
llm = Ollama(
|
||||
model=config['model'],
|
||||
base_url=f"http://{config['host']}:{config['port']}",
|
||||
timeout=120.0 # 120秒超时
|
||||
)
|
||||
|
||||
# 设置全局LLM
|
||||
Settings.llm = llm
|
||||
|
||||
# 创建ChatMemoryBuffer(设置足够的token_limit确保保留30+轮对话)
|
||||
memory = ChatMemoryBuffer.from_defaults(token_limit=8000)
|
||||
|
||||
# 从配置文件加载系统提示词(如果存在),否则使用默认值并保存
|
||||
system_prompt = self._get_system_prompt()
|
||||
|
||||
# 创建对话引擎
|
||||
# 由于llama_index的API可能在不同版本有变化,这里使用基本的chat接口
|
||||
# 实际使用时可能需要根据llama_index的版本调整
|
||||
chat_engine = {
|
||||
'llm': llm,
|
||||
'memory': memory,
|
||||
'system_prompt': system_prompt
|
||||
}
|
||||
|
||||
# 存储到全局字典
|
||||
_chat_engines[chat_id] = chat_engine
|
||||
|
||||
return chat_engine
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"导入llama_index模块失败: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"创建ChatEngine失败: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def _get_user_role(self, chat_id: int, user_id: int) -> str:
|
||||
"""获取用户角色名称(创建或获取映射)
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
角色名称
|
||||
"""
|
||||
# 获取现有映射
|
||||
user_mapping, user_count = self._get_user_mapping(chat_id)
|
||||
|
||||
user_id_str = str(user_id)
|
||||
|
||||
# 如果用户已存在,返回角色名称
|
||||
if user_id_str in user_mapping:
|
||||
return user_mapping[user_id_str]
|
||||
|
||||
# 新用户,分配角色
|
||||
user_count += 1
|
||||
role_name = f"用户{user_count}"
|
||||
user_mapping[user_id_str] = role_name
|
||||
|
||||
# 保存到数据库
|
||||
state_data = {
|
||||
"user_mapping": user_mapping,
|
||||
"user_count": user_count
|
||||
}
|
||||
self.db.save_game_state(chat_id, 0, 'ai_chat', state_data)
|
||||
|
||||
return role_name
|
||||
|
||||
def _get_user_mapping(self, chat_id: int) -> tuple[Dict[str, str], int]:
|
||||
"""获取用户角色映射和计数
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
|
||||
Returns:
|
||||
(用户映射字典, 用户计数)
|
||||
"""
|
||||
# 从数据库获取映射
|
||||
state = self.db.get_game_state(chat_id, 0, 'ai_chat')
|
||||
|
||||
if state and state.get('state_data'):
|
||||
user_mapping = state['state_data'].get('user_mapping', {})
|
||||
user_count = state['state_data'].get('user_count', 0)
|
||||
else:
|
||||
user_mapping = {}
|
||||
user_count = 0
|
||||
|
||||
return user_mapping, user_count
|
||||
|
||||
def _get_default_system_prompt(self) -> str:
|
||||
"""获取默认系统提示词
|
||||
|
||||
Returns:
|
||||
默认系统提示词
|
||||
"""
|
||||
return (
|
||||
"你是WPS游戏机器人的AI助手,这是一个多功能的游戏和娱乐机器人系统。"
|
||||
"这是一个多用户对话场景,不同用户的发言会用不同的角色标识(如'用户1'、'用户2'等)。"
|
||||
"你需要理解不同用户的发言内容,并根据上下文给出合适的回复。"
|
||||
"\n\n"
|
||||
"**你的身份和职责:**\n"
|
||||
"1. 你是WPS协作平台的游戏机器人AI助手\n"
|
||||
"2. 你可以帮助用户了解和使用机器人的各种功能\n"
|
||||
"3. 你可以回答用户的问题,提供游戏指导和建议\n"
|
||||
"4. 当用户询问功能时,主动推荐相关游戏和功能\n"
|
||||
"\n\n"
|
||||
"**机器人支持的功能:**\n"
|
||||
"1. 🎲 骰娘系统:`.r XdY` - 掷骰子游戏\n"
|
||||
"2. ✊ 石头剪刀布:`.rps 石头/剪刀/布` - 对战游戏\n"
|
||||
"3. 🔮 运势占卜:`.fortune` 或 `.运势` - 查看今日运势\n"
|
||||
"4. 🔢 猜数字:`.guess start` - 开始猜数字游戏\n"
|
||||
"5. 📝 问答游戏:`.quiz` - 回答问题挑战\n"
|
||||
"6. 🀄 成语接龙:`.idiom start` - 成语接龙游戏\n"
|
||||
"7. ⚫ 五子棋:`.gomoku challenge` - 发起五子棋对战\n"
|
||||
"8. 💎 积分系统:`.points` - 查看积分,`.checkin` - 每日签到\n"
|
||||
"9. ⚗️ 炼金系统:`.alchemy` - 消耗积分进行炼金\n"
|
||||
"10. ⚡️ 冒险系统:`.adventure` - 消耗时间进行冒险\n"
|
||||
"11. 🎁 积分赠送:`.gift <用户ID> <积分>` - 赠送积分给其他用户\n"
|
||||
"12. 📊 统计信息:`.stats` - 查看个人游戏统计\n"
|
||||
"13. ❓ 帮助信息:`.help` - 查看完整的帮助文档\n"
|
||||
"\n\n"
|
||||
"**回复指南:**\n"
|
||||
"- 用自然、友好、热情的方式与用户交流\n"
|
||||
"- 当用户不知道玩什么时,主动推荐适合的游戏\n"
|
||||
"- 详细解释功能的使用方法和规则\n"
|
||||
"- 鼓励用户尝试不同的游戏和功能\n"
|
||||
"- 如果是多人场景,可以推荐适合多人参与的游戏(如成语接龙、五子棋)\n"
|
||||
"- 记住用户的偏好和之前的对话内容,提供个性化建议\n"
|
||||
)
|
||||
|
||||
def _get_system_prompt(self) -> str:
|
||||
"""获取系统提示词(从配置文件加载,如果不存在则使用默认值并保存)
|
||||
|
||||
Returns:
|
||||
系统提示词
|
||||
"""
|
||||
config = self._load_config()
|
||||
|
||||
# 如果配置中存在系统提示词,直接返回
|
||||
if 'system_prompt' in config and config['system_prompt']:
|
||||
return config['system_prompt']
|
||||
|
||||
# 否则使用默认值并保存到配置文件
|
||||
default_prompt = self._get_default_system_prompt()
|
||||
config['system_prompt'] = default_prompt
|
||||
self._save_config(config)
|
||||
|
||||
return default_prompt
|
||||
|
||||
def _load_config(self) -> Dict[str, Any]:
|
||||
"""从JSON文件加载配置
|
||||
|
||||
Returns:
|
||||
配置字典
|
||||
"""
|
||||
# 如果文件不存在,创建默认配置
|
||||
if not self.config_file.exists():
|
||||
default_config = {
|
||||
"host": "localhost",
|
||||
"port": 11434,
|
||||
"model": "llama3.1"
|
||||
}
|
||||
self._save_config(default_config)
|
||||
return default_config
|
||||
|
||||
try:
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# 确保所有必需的字段存在
|
||||
if 'host' not in config:
|
||||
config['host'] = "localhost"
|
||||
if 'port' not in config:
|
||||
config['port'] = 11434
|
||||
if 'model' not in config:
|
||||
config['model'] = "llama3.1"
|
||||
|
||||
return config
|
||||
except Exception as e:
|
||||
logger.error(f"加载配置文件失败: {e}", exc_info=True)
|
||||
# 返回默认配置
|
||||
return {
|
||||
"host": "localhost",
|
||||
"port": 11434,
|
||||
"model": "llama3.1"
|
||||
}
|
||||
|
||||
def _save_config(self, config: Dict[str, Any]) -> bool:
|
||||
"""保存配置到JSON文件
|
||||
|
||||
Args:
|
||||
config: 配置字典
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 写入JSON文件
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=4, ensure_ascii=False)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存配置文件失败: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def get_help(self) -> str:
|
||||
"""获取帮助信息
|
||||
|
||||
Returns:
|
||||
帮助文本
|
||||
"""
|
||||
return """## 🤖 AI对话系统帮助
|
||||
|
||||
### 基本用法
|
||||
- `.ai <问题>` - 向AI提问(支持多用户对话,等待10秒后回答)
|
||||
- `.aiconfig host=xxx port=xxx model=xxx` - 配置Ollama服务地址和模型
|
||||
|
||||
### 配置示例
|
||||
`.aiconfig host=localhost port=11434 model=llama3.1`
|
||||
|
||||
### 说明
|
||||
- 多个用户可以在同一个会话中提问
|
||||
- 系统会等待10秒,收集所有问题后统一回答
|
||||
- 如果在等待期间有新消息,会重新计时
|
||||
|
||||
---
|
||||
💡 提示:确保Ollama服务已启动并配置正确
|
||||
"""
|
||||
|
||||
207
games/alchemy.py
Normal file
207
games/alchemy.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""炼金系统游戏模块"""
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from games.base import BaseGame
|
||||
from utils.parser import CommandParser
|
||||
from core.database import get_db
|
||||
from typing import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AlchemyGame(BaseGame):
|
||||
"""炼金系统游戏"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化游戏"""
|
||||
super().__init__()
|
||||
self.db = get_db()
|
||||
|
||||
# 奖品池配置 - 确保数学期望等于消耗积分
|
||||
self.prize_pool: List[Tuple[int, str, float, str]] = [
|
||||
# (权重, 类型, 倍率, 描述)
|
||||
(500, "penalty", 0, "炼金失败"),
|
||||
(160, "penalty", -1, "炼金爆炸"),
|
||||
(110, "points", 0.1, "少量积分"),
|
||||
(390, "points", 0.5, "少量积分"),
|
||||
(500, "points", 1, "等值积分"),
|
||||
(390, "points", 2, "丰厚积分"),
|
||||
(136, "points", 5, "丰厚积分"),
|
||||
(9, "points", 10, "🌟 巨额积分"),
|
||||
(1, "points", 100, "💎 传说积分"),
|
||||
]
|
||||
|
||||
self.total_weight: int = 0
|
||||
for weight,_,_,_ in self.prize_pool:
|
||||
self.total_weight += weight
|
||||
|
||||
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理炼金相关指令
|
||||
|
||||
Args:
|
||||
command: 指令,如 ".alchemy", ".alchemy 10", ".alchemy stats"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
回复消息
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip().lower()
|
||||
|
||||
|
||||
# 炼金说明
|
||||
if args in ['help', '帮助', 'info']:
|
||||
return self._get_alchemy_help()
|
||||
|
||||
# 默认:炼金抽奖
|
||||
else:
|
||||
# 解析消耗积分数量
|
||||
cost_points = 10 # 默认消耗10积分
|
||||
if args.isdigit():
|
||||
cost_points = int(args)
|
||||
|
||||
return self._perform_alchemy(user_id, cost_points)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理炼金指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
def _perform_alchemy(self, user_id: int, cost_points: int) -> str:
|
||||
"""执行炼金抽奖
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
cost_points: 消耗积分
|
||||
|
||||
Returns:
|
||||
抽奖结果消息
|
||||
"""
|
||||
# 检查用户是否正在冒险中
|
||||
state = self.db.get_game_state(0, user_id, 'adventure')
|
||||
if state:
|
||||
try:
|
||||
state_data = state['state_data']
|
||||
start_time = state_data.get('start_time', 0)
|
||||
cost_time = state_data.get('cost_time', 1)
|
||||
current_time = int(time.time())
|
||||
end_time = start_time + cost_time * 60
|
||||
remaining_seconds = end_time - current_time
|
||||
|
||||
# 如果冒险已完成,提示用户先回收奖励,不允许炼金
|
||||
if remaining_seconds <= 0:
|
||||
return f"❌ 你有待回收的冒险奖励!\n\n💡 请先使用 `.adventure` 回收冒险奖励后再进行炼金。"
|
||||
else:
|
||||
# 冒险未完成,返回错误提示
|
||||
remaining_minutes = remaining_seconds // 60
|
||||
remaining_secs = remaining_seconds % 60
|
||||
|
||||
if remaining_minutes > 0:
|
||||
wait_msg = f"{remaining_minutes} 分 {remaining_secs} 秒"
|
||||
else:
|
||||
wait_msg = f"{remaining_secs} 秒"
|
||||
|
||||
return f"❌ 冒险进行中,无法进行炼金!\n\n还需等待 **{wait_msg}** 才能完成冒险。\n\n💡 提示:冒险期间无法进行炼金,请先完成冒险!"
|
||||
except Exception as e:
|
||||
# 状态数据异常,删除损坏状态,允许继续
|
||||
logger.error(f"冒险状态数据异常: {e}", exc_info=True)
|
||||
self.db.delete_game_state(0, user_id, 'adventure')
|
||||
|
||||
# 检查用户积分是否足够
|
||||
user_points = self.db.get_user_points(user_id)
|
||||
if user_points['points'] < cost_points:
|
||||
return f"❌ 积分不足!需要 {cost_points} 积分,当前可用 {user_points['points']} 积分"
|
||||
|
||||
# 选择奖品池
|
||||
prize_pool = self.prize_pool
|
||||
|
||||
# 执行抽奖
|
||||
reward = self._draw_prize(prize_pool)
|
||||
reward_points = int(reward['value']*cost_points)
|
||||
|
||||
# 消费积分
|
||||
if not self.db.consume_points(user_id, cost_points, "alchemy", f"炼金抽奖消耗"):
|
||||
return "❌ 积分消费失败,请稍后重试"
|
||||
|
||||
# 处理奖励
|
||||
if reward['type'] == 'points' and reward['value'] > 0:
|
||||
# 获得积分奖励
|
||||
self.db.add_points(user_id, reward_points, "alchemy", f"炼金奖励")
|
||||
elif reward['type'] == 'penalty' and reward['value'] < 0:
|
||||
# 负面奖励(扣分)
|
||||
penalty_points = abs(reward_points)
|
||||
self.db.consume_points(user_id, penalty_points, "alchemy", f"炼金失败")
|
||||
|
||||
# 炼金系统已简化,不再记录历史
|
||||
|
||||
# 获取更新后的积分信息
|
||||
updated_points = self.db.get_user_points(user_id)
|
||||
|
||||
# 格式化输出
|
||||
text = f"## ⚗️ 炼金结果\n\n"
|
||||
text += f"**消耗积分**:{cost_points} 分\n\n"
|
||||
|
||||
if reward['type'] == 'points':
|
||||
text += f"**{reward['description']}**: 获得{reward_points} 积分\n\n"
|
||||
elif reward['type'] == 'penalty':
|
||||
text += f"**{reward['description']}**: 损失 {abs(reward_points)} 积分\n\n"
|
||||
|
||||
text += f"**当前积分**:{updated_points['points']} 分\n\n"
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:炼金有风险,投资需谨慎!"
|
||||
|
||||
return text
|
||||
|
||||
def _draw_prize(self, prize_pool: list) -> dict:
|
||||
"""从奖品池中抽取奖品
|
||||
|
||||
Args:
|
||||
prize_pool: 奖品池
|
||||
|
||||
Returns:
|
||||
奖品信息
|
||||
"""
|
||||
# 生成随机数
|
||||
rand = random.random()*self.total_weight
|
||||
cumulative_prob = 0.0
|
||||
|
||||
for weight, reward_type, reward_value, description in prize_pool:
|
||||
cumulative_prob += weight
|
||||
if rand <= cumulative_prob:
|
||||
return {
|
||||
'type': reward_type,
|
||||
'value': reward_value,
|
||||
'description': description
|
||||
}
|
||||
|
||||
# 兜底返回第一个奖品
|
||||
return {
|
||||
'type': prize_pool[0][1],
|
||||
'value': prize_pool[0][2],
|
||||
'description': prize_pool[0][3]
|
||||
}
|
||||
|
||||
def _get_alchemy_help(self) -> str:
|
||||
"""获取炼金帮助信息
|
||||
|
||||
Returns:
|
||||
帮助信息消息
|
||||
"""
|
||||
text = f"## ⚗️ 炼金系统\n\n"
|
||||
text += f"### 基础用法\n"
|
||||
text += f"- `.alchemy` - 消耗10积分进行炼金\n"
|
||||
text += f"- `.alchemy cost` - 消耗cost积分进行炼金, 最少1积分\n"
|
||||
|
||||
text += f"### 其他功能\n"
|
||||
text += f"- `.alchemy help` - 查看帮助\n\n"
|
||||
|
||||
return text
|
||||
|
||||
def get_help(self) -> str:
|
||||
"""获取帮助信息"""
|
||||
return self._get_alchemy_help()
|
||||
|
||||
@@ -64,6 +64,92 @@ def get_help_message() -> str:
|
||||
- `.quiz` - 随机问题
|
||||
- `.quiz 答案` - 回答问题
|
||||
|
||||
### 🀄 成语接龙
|
||||
- `.idiom start [成语]` - 开始游戏
|
||||
- `.idiom [成语]` - 接龙
|
||||
- `.idiom [成语] @某人` - 接龙并指定下一位
|
||||
- `.idiom stop` - 结束游戏
|
||||
- `.idiom status` - 查看状态
|
||||
- `.idiom reject [词语]` - 拒绝词语加入黑名单(仅发起人)
|
||||
- `.idiom blacklist` - 查看黑名单
|
||||
|
||||
### ⚫ 五子棋
|
||||
- `.gomoku challenge` - 发起挑战
|
||||
- `.gomoku accept` - 接受挑战
|
||||
- `.gomoku A1` - 落子
|
||||
- `.gomoku show` - 显示棋盘
|
||||
- `.gomoku resign` - 认输
|
||||
- `.gomoku list` - 列出所有对战
|
||||
- `.gomoku stats` - 查看战绩
|
||||
|
||||
### 💎 积分系统
|
||||
- `.points` - 查看个人积分
|
||||
- `.积分` - 查看个人积分
|
||||
- `.checkin` - 每日签到
|
||||
- `.签到` - 每日签到
|
||||
- `.打卡` - 每日签到
|
||||
- `.points leaderboard` - 积分排行榜
|
||||
|
||||
### ⚗️ 炼金系统
|
||||
- `.alchemy` - 消耗10积分进行炼金
|
||||
- `.炼金` - 消耗10积分进行炼金
|
||||
- `.alchemy 20` - 消耗20积分进行炼金
|
||||
- `.alchemy 50` - 消耗50积分进行炼金
|
||||
|
||||
### ⚡️ 冒险系统
|
||||
- `.adventure` - 消耗1分钟进行冒险
|
||||
- `.冒险` - 消耗1分钟进行冒险
|
||||
- `.adventure 5` - 消耗5分钟进行冒险
|
||||
- `.adventure abandon` - 放弃当前冒险,按最低倍率结算已冒险时间
|
||||
- `.adventure 放弃` - 放弃当前冒险,按最低倍率结算已冒险时间
|
||||
- `.adventure help` - 查看冒险帮助
|
||||
|
||||
### 🎁 积分赠送系统
|
||||
- `.gift <用户ID> <积分数量> [消息]` - 赠送积分
|
||||
- `.赠送 <用户ID> <积分数量> [消息]` - 赠送积分
|
||||
- `.送 <用户ID> <积分数量> [消息]` - 赠送积分
|
||||
|
||||
### 🤖 AI对话系统
|
||||
- `.ai <问题>` - 向AI提问(支持多用户对话,等待10秒后回答)
|
||||
- `.aiconfig host=xxx port=xxx model=xxx` - 配置Ollama服务地址和模型
|
||||
|
||||
### 🎰 赌场系统
|
||||
**大小游戏**:
|
||||
- `.赌场 大小 open <最小> <最大> <赔率>` - 庄家开启大小游戏
|
||||
- `.赌场 大小 bet <大/小> <金额>` - 下注
|
||||
- `.赌场 大小 status` - 查看状态
|
||||
- `.赌场 大小 settle` - 庄家结算(系统随机)
|
||||
- `.赌场 大小 cancel` - 庄家放弃游戏(返还下注)
|
||||
|
||||
**轮盘游戏**:
|
||||
- `.赌场 轮盘 open <最小> <最大>` - 庄家开启轮盘游戏
|
||||
- `.赌场 轮盘 bet <类型> <选项> <金额>` - 下注(数字/颜色/奇偶/大小/区间)
|
||||
- `.赌场 轮盘 status` - 查看状态
|
||||
- `.赌场 轮盘 settle` - 庄家结算(系统随机0-36)
|
||||
- `.赌场 轮盘 cancel` - 庄家放弃游戏(返还下注)
|
||||
|
||||
**21点游戏**:
|
||||
- `.赌场 21点 open <最小> <最大> [黑杰克倍数]` - 庄家开启21点游戏
|
||||
- `.赌场 21点 bet <金额>` - 下注
|
||||
- `.赌场 21点 deal` - 庄家发牌
|
||||
- `.赌场 21点 hit` - 玩家要牌
|
||||
- `.赌场 21点 stand` - 玩家停牌
|
||||
- `.赌场 21点 status` - 查看状态
|
||||
- `.赌场 21点 settle` - 庄家结算
|
||||
- `.赌场 21点 cancel` - 庄家放弃游戏(返还下注)
|
||||
|
||||
### 🐺 狼人杀
|
||||
- `.狼人杀 open` - 主持人创建房间
|
||||
- `.狼人杀 join` - 加入游戏
|
||||
- `.狼人杀 start` - 主持人开始游戏
|
||||
- `.狼人杀 <id> <消息>` - 私聊指定玩家
|
||||
- `.狼人杀 狼人 <消息>` - 狼人群聊
|
||||
- `.狼人杀 杀 <id>` - 狼人投票杀人
|
||||
- `.狼人杀 验 <id>` - 预言家验人
|
||||
- `.狼人杀 救 <id>` - 女巫救人
|
||||
- `.狼人杀 毒 <id>` - 女巫毒人
|
||||
- `.狼人杀 status` - 查看状态
|
||||
|
||||
### 其他
|
||||
- `.help` - 显示帮助
|
||||
- `.stats` - 查看个人统计
|
||||
@@ -95,7 +181,9 @@ def get_stats_message(user_id: int) -> str:
|
||||
game_names = {
|
||||
'rps': '✊ 石头剪刀布',
|
||||
'guess': '🔢 猜数字',
|
||||
'quiz': '📝 问答游戏'
|
||||
'quiz': '📝 问答游戏',
|
||||
'idiom': '🀄 成语接龙',
|
||||
'gomoku': '⚫ 五子棋'
|
||||
}
|
||||
|
||||
for row in stats:
|
||||
|
||||
1473
games/casino.py
Normal file
1473
games/casino.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from games.base import BaseGame
|
||||
from utils.parser import CommandParser
|
||||
from games.points import PointsGame
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,6 +20,7 @@ class FortuneGame(BaseGame):
|
||||
super().__init__()
|
||||
self._fortunes = None
|
||||
self._tarot = None
|
||||
self.points_game = PointsGame()
|
||||
|
||||
def _load_data(self):
|
||||
"""懒加载运势数据"""
|
||||
@@ -95,6 +97,11 @@ class FortuneGame(BaseGame):
|
||||
# 重置随机seed
|
||||
random.seed()
|
||||
|
||||
# 尝试获得积分奖励(30%概率)
|
||||
points_earned = 0
|
||||
if random.random() < 0.3: # 30%概率获得积分
|
||||
points_earned = self.points_game.add_fortune_points(user_id)
|
||||
|
||||
# 格式化输出
|
||||
text = f"## 🔮 今日运势\n\n"
|
||||
text += f"**日期**:{today}\n\n"
|
||||
@@ -103,8 +110,13 @@ class FortuneGame(BaseGame):
|
||||
text += f"**建议**:{fortune['advice']}\n\n"
|
||||
text += f"**幸运数字**:{lucky_number}\n\n"
|
||||
text += f"**幸运颜色**:{lucky_color}\n\n"
|
||||
|
||||
# 添加积分奖励信息
|
||||
if points_earned > 0:
|
||||
text += f"**🎁 积分奖励**:+{points_earned} 分\n\n"
|
||||
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:运势仅供娱乐参考~"
|
||||
text += "💡 提示:运势仅供娱乐参考,查看运势有机会获得积分奖励~"
|
||||
|
||||
return text
|
||||
|
||||
|
||||
263
games/gift.py
Normal file
263
games/gift.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""积分赠送系统游戏模块"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from games.base import BaseGame
|
||||
from utils.parser import CommandParser
|
||||
from core.database import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GiftGame(BaseGame):
|
||||
"""积分赠送系统游戏"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化游戏"""
|
||||
super().__init__()
|
||||
self.db = get_db()
|
||||
|
||||
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理积分赠送相关指令
|
||||
|
||||
Args:
|
||||
command: 指令,如 ".gift 123 50 生日快乐", ".gift sent", ".gift received"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
回复消息
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
|
||||
# 赠送帮助
|
||||
if args in ['help', '帮助']:
|
||||
return self._get_gift_help()
|
||||
|
||||
# 默认:执行赠送
|
||||
else:
|
||||
return self._process_gift_command(args, user_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理积分赠送指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
def _process_gift_command(self, args: str, sender_id: int) -> str:
|
||||
"""处理赠送指令
|
||||
|
||||
Args:
|
||||
args: 指令参数
|
||||
sender_id: 发送者ID
|
||||
|
||||
Returns:
|
||||
处理结果消息
|
||||
"""
|
||||
# 解析参数:.gift <receiver_identifier> <points> [message]
|
||||
parts = args.split(maxsplit=2)
|
||||
|
||||
if len(parts) < 2:
|
||||
return "❌ 指令格式错误!\n\n正确格式:`.gift <用户名或ID> <积分数量> [附赠消息]`\n\n示例:\n`.gift 张三 50 生日快乐`\n`.gift 123 50`\n`.gift 456 100`"
|
||||
|
||||
# 解析积分数量
|
||||
try:
|
||||
points = int(parts[1])
|
||||
message = parts[2] if len(parts) > 2 else None
|
||||
except ValueError:
|
||||
return "❌ 积分数量必须是数字!"
|
||||
|
||||
# 检查第一部分是用户名还是ID
|
||||
receiver_input = parts[0]
|
||||
|
||||
if receiver_input.isdigit():
|
||||
# 是数字,作为用户ID处理
|
||||
receiver_id = int(receiver_input)
|
||||
else:
|
||||
# 是用户名,通过数据库查找
|
||||
user = self.db.get_user_by_name(receiver_input)
|
||||
if not user:
|
||||
return f"❌ 未找到用户: {receiver_input}\n\n请确认用户名是否正确,或使用用户ID。"
|
||||
receiver_id = user['user_id']
|
||||
|
||||
# 获取接收者名称用于显示
|
||||
receiver_name = self.db.get_user_display_name(receiver_id)
|
||||
|
||||
# 获取发送者名称用于显示
|
||||
sender_name = self.db.get_user_display_name(sender_id)
|
||||
|
||||
# 验证参数
|
||||
if points <= 0:
|
||||
return "❌ 赠送积分数量必须大于0!"
|
||||
|
||||
if sender_id == receiver_id:
|
||||
return "❌ 不能赠送积分给自己!"
|
||||
|
||||
# 检查赠送者积分是否足够
|
||||
sender_points = self.db.get_user_points(sender_id)
|
||||
if sender_points['points'] < points:
|
||||
return f"❌ 积分不足!需要 {points} 积分,当前可用 {sender_points['points']} 积分"
|
||||
|
||||
# 执行赠送(消费赠送者积分,增加接收者积分)
|
||||
if (self.db.consume_points(sender_id, points, "gift_send", f"赠送积分给用户{receiver_id}") and
|
||||
self.db.add_points(receiver_id, points, "gift_receive", f"收到用户{sender_id}的积分赠送")):
|
||||
|
||||
# 获取更新后的积分信息
|
||||
sender_points_after = self.db.get_user_points(sender_id)
|
||||
receiver_points_after = self.db.get_user_points(receiver_id)
|
||||
|
||||
text = f"## 🎁 积分赠送成功!\n\n"
|
||||
text += f"**赠送者**:{sender_name}\n\n"
|
||||
text += f"**接收者**:{receiver_name}\n\n"
|
||||
text += f"**赠送积分**:{points} 分\n\n"
|
||||
|
||||
if message:
|
||||
text += f"**附赠消息**:{message}\n\n"
|
||||
|
||||
text += f"**赠送者剩余积分**:{sender_points_after['points']} 分\n\n"
|
||||
text += f"**接收者当前积分**:{receiver_points_after['points']} 分\n\n"
|
||||
text += "---\n\n"
|
||||
text += "💝 感谢您的慷慨赠送!"
|
||||
|
||||
return text
|
||||
else:
|
||||
return "❌ 赠送失败!请检查积分是否足够或用户ID是否正确。"
|
||||
|
||||
def _get_gift_stats(self, user_id: int) -> str:
|
||||
"""获取赠送统计信息
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
统计信息消息
|
||||
"""
|
||||
stats = self.db.get_gift_stats(user_id)
|
||||
|
||||
if stats['sent_count'] == 0 and stats['received_count'] == 0:
|
||||
return "📊 你还没有任何积分赠送记录哦~"
|
||||
|
||||
text = f"## 🎁 积分赠送统计\n\n"
|
||||
text += f"**发送统计**:\n"
|
||||
text += f"- 赠送次数:{stats['sent_count']} 次\n"
|
||||
text += f"- 赠送积分:{stats['total_sent']} 分\n\n"
|
||||
|
||||
text += f"**接收统计**:\n"
|
||||
text += f"- 接收次数:{stats['received_count']} 次\n"
|
||||
text += f"- 接收积分:{stats['total_received']} 分\n\n"
|
||||
|
||||
net_gift = stats['net_gift']
|
||||
if net_gift > 0:
|
||||
text += f"**净收益**:+{net_gift} 分 🎉\n\n"
|
||||
elif net_gift < 0:
|
||||
text += f"**净收益**:{net_gift} 分 😅\n\n"
|
||||
else:
|
||||
text += f"**净收益**:0 分 ⚖️\n\n"
|
||||
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:使用 `.gift sent` 查看发送记录,`.gift received` 查看接收记录"
|
||||
|
||||
return text
|
||||
|
||||
def _get_gift_records_sent(self, user_id: int, limit: int = 10) -> str:
|
||||
"""获取发送的赠送记录
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
limit: 限制数量
|
||||
|
||||
Returns:
|
||||
记录信息消息
|
||||
"""
|
||||
records = self.db.get_gift_records_sent(user_id, limit)
|
||||
|
||||
if not records:
|
||||
return "📝 暂无发送记录"
|
||||
|
||||
text = f"## 🎁 发送记录(最近 {len(records)} 条)\n\n"
|
||||
|
||||
for record in records:
|
||||
timestamp = datetime.fromtimestamp(record['created_at']).strftime('%m-%d %H:%M')
|
||||
receiver_name = self.db.get_user_display_name(record['receiver_id'])
|
||||
points = record['points']
|
||||
message = record.get('message', '')
|
||||
|
||||
text += f"**{timestamp}** 赠送 {points} 分给 {receiver_name}\n"
|
||||
if message:
|
||||
text += f" 💬 {message}\n"
|
||||
text += "\n"
|
||||
|
||||
return text
|
||||
|
||||
def _get_gift_records_received(self, user_id: int, limit: int = 10) -> str:
|
||||
"""获取接收的赠送记录
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
limit: 限制数量
|
||||
|
||||
Returns:
|
||||
记录信息消息
|
||||
"""
|
||||
records = self.db.get_gift_records_received(user_id, limit)
|
||||
|
||||
if not records:
|
||||
return "📝 暂无接收记录"
|
||||
|
||||
text = f"## 🎁 接收记录(最近 {len(records)} 条)\n\n"
|
||||
|
||||
for record in records:
|
||||
timestamp = datetime.fromtimestamp(record['created_at']).strftime('%m-%d %H:%M')
|
||||
sender_name = self.db.get_user_display_name(record['sender_id'])
|
||||
points = record['points']
|
||||
message = record.get('message', '')
|
||||
|
||||
text += f"**{timestamp}** 收到 {sender_name} 的 {points} 分\n"
|
||||
if message:
|
||||
text += f" 💬 {message}\n"
|
||||
text += "\n"
|
||||
|
||||
return text
|
||||
|
||||
def _get_gift_help(self) -> str:
|
||||
"""获取赠送帮助信息
|
||||
|
||||
Returns:
|
||||
帮助信息消息
|
||||
"""
|
||||
text = f"## 🎁 积分赠送系统\n\n"
|
||||
text += f"### 基础用法\n"
|
||||
text += f"- `.gift <用户名或ID> <积分数量> [附赠消息]` - 赠送积分\n"
|
||||
text += f"- `.gift stats` - 查看赠送统计\n"
|
||||
text += f"- `.gift sent` - 查看发送记录\n"
|
||||
text += f"- `.gift received` - 查看接收记录\n"
|
||||
text += f"- `.gift help` - 查看帮助\n\n"
|
||||
|
||||
text += f"### 赠送规则\n"
|
||||
text += f"- **积分限制**:单次最多赠送1000积分\n"
|
||||
text += f"- **自赠限制**:不能赠送给自己\n"
|
||||
text += f"- **积分检查**:必须有足够积分才能赠送\n"
|
||||
text += f"- **附赠消息**:可选,最多100字符\n\n"
|
||||
|
||||
text += f"### 示例\n"
|
||||
text += f"```\n"
|
||||
text += f".gift 张三 50 生日快乐\n"
|
||||
text += f".gift 123 50 (使用用户ID)\n"
|
||||
text += f".gift 李四 100 感谢你的帮助\n"
|
||||
text += f".gift 王五 200\n"
|
||||
text += f".gift stats\n"
|
||||
text += f"```\n\n"
|
||||
|
||||
text += f"### 说明\n"
|
||||
text += f"- 支持使用用户名或用户ID进行赠送\n"
|
||||
text += f"- 使用用户名需要先通过 `.register` 注册名称\n"
|
||||
text += f"- 所有赠送都有完整记录\n"
|
||||
text += f"- 赠送和接收都会在积分记录中显示\n"
|
||||
text += f"- 赠送是单向的,无需对方确认\n"
|
||||
|
||||
return text
|
||||
|
||||
def get_help(self) -> str:
|
||||
"""获取帮助信息"""
|
||||
return self._get_gift_help()
|
||||
567
games/gomoku.py
Normal file
567
games/gomoku.py
Normal file
@@ -0,0 +1,567 @@
|
||||
"""五子棋游戏"""
|
||||
import time
|
||||
import re
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
from games.base import BaseGame
|
||||
from games import gomoku_logic as logic
|
||||
from utils.parser import CommandParser
|
||||
from config import GAME_CONFIG
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GomokuGame(BaseGame):
|
||||
"""五子棋游戏"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化游戏"""
|
||||
super().__init__()
|
||||
self.config = GAME_CONFIG.get('gomoku', {})
|
||||
self.max_concurrent_games = self.config.get('max_concurrent_games', 5)
|
||||
self.board_size = self.config.get('board_size', 15)
|
||||
|
||||
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理五子棋指令
|
||||
|
||||
Args:
|
||||
command: 指令,如 ".gomoku @对手" 或 ".gomoku A1"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
回复消息
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
# 调试日志
|
||||
logger.info(f"五子棋指令解析 - command: {command}")
|
||||
logger.info(f"五子棋指令解析 - args: {args}")
|
||||
|
||||
# 没有参数,显示帮助
|
||||
if not args:
|
||||
return self.get_help()
|
||||
|
||||
# 解析参数
|
||||
parts = args.split(maxsplit=1)
|
||||
action = parts[0].lower()
|
||||
logger.info(f"五子棋指令解析 - action: {action}")
|
||||
|
||||
# 帮助
|
||||
if action in ['help', '帮助']:
|
||||
return self.get_help()
|
||||
|
||||
# 发起挑战
|
||||
if action in ['challenge', 'start', '挑战', '开始']:
|
||||
return self._create_challenge(chat_id, user_id)
|
||||
|
||||
# 接受挑战
|
||||
if action in ['accept', 'join', '接受', '加入']:
|
||||
return self._accept_challenge(chat_id, user_id)
|
||||
|
||||
# 取消挑战
|
||||
if action in ['cancel', '取消']:
|
||||
return self._cancel_challenge(chat_id, user_id)
|
||||
|
||||
# 列出所有对战
|
||||
if action in ['list', '列表', '查看']:
|
||||
return self._list_games(chat_id)
|
||||
|
||||
# 查看战绩
|
||||
if action in ['stats', '战绩', '统计']:
|
||||
return self._get_stats(user_id)
|
||||
|
||||
# 显示棋盘
|
||||
if action in ['show', '显示', '棋盘']:
|
||||
return self._show_board(chat_id, user_id)
|
||||
|
||||
# 认输
|
||||
if action in ['resign', '认输', '投降']:
|
||||
return self._resign(chat_id, user_id)
|
||||
|
||||
# 尝试解析为坐标(落子)
|
||||
coord = logic.parse_coord(action)
|
||||
if coord is not None:
|
||||
return self._make_move(chat_id, user_id, action)
|
||||
|
||||
# 未识别的指令
|
||||
logger.warning(f"五子棋未识别的指令 - args: {args}")
|
||||
return f"❌ 未识别的指令:{args}\n\n💡 提示:\n- 发起挑战:`.gomoku challenge`\n- 接受挑战:`.gomoku accept`\n- 落子:`.gomoku A1`\n- 查看帮助:`.gomoku help`"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理五子棋指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
def _get_game_pool(self, chat_id: int) -> Dict[str, Any]:
|
||||
"""获取游戏池
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
|
||||
Returns:
|
||||
游戏池数据
|
||||
"""
|
||||
state = self.db.get_game_state(chat_id, 0, 'gomoku')
|
||||
if state:
|
||||
return state['state_data']
|
||||
else:
|
||||
return {
|
||||
"games": [],
|
||||
"max_concurrent_games": self.max_concurrent_games
|
||||
}
|
||||
|
||||
def _save_game_pool(self, chat_id: int, pool_data: Dict[str, Any]):
|
||||
"""保存游戏池
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
pool_data: 游戏池数据
|
||||
"""
|
||||
self.db.save_game_state(chat_id, 0, 'gomoku', pool_data)
|
||||
|
||||
def _find_user_game(self, chat_id: int, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""查找用户参与的游戏
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
游戏数据或None
|
||||
"""
|
||||
pool = self._get_game_pool(chat_id)
|
||||
|
||||
for game in pool.get("games", []):
|
||||
if game["status"] == "playing":
|
||||
if game["player_black"] == user_id or game["player_white"] == user_id:
|
||||
return game
|
||||
|
||||
return None
|
||||
|
||||
def _create_challenge(self, chat_id: int, user_id: int) -> str:
|
||||
"""创建挑战
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 发起者ID
|
||||
|
||||
Returns:
|
||||
提示消息
|
||||
"""
|
||||
# 获取游戏池
|
||||
pool = self._get_game_pool(chat_id)
|
||||
games = pool.get("games", [])
|
||||
challenges = pool.get("challenges", [])
|
||||
|
||||
# 检查用户是否已经在对战中
|
||||
user_game = self._find_user_game(chat_id, user_id)
|
||||
if user_game:
|
||||
return "⚠️ 你已经在对战中!\n\n输入 `.gomoku show` 查看棋盘"
|
||||
|
||||
# 检查用户是否已经发起了挑战
|
||||
for challenge in challenges:
|
||||
if challenge["challenger_id"] == user_id:
|
||||
return "⚠️ 你已经发起了一个挑战!\n\n等待其他人接受,或输入 `.gomoku cancel` 取消挑战"
|
||||
|
||||
# 创建挑战
|
||||
current_time = int(time.time())
|
||||
challenge = {
|
||||
"challenger_id": user_id,
|
||||
"created_at": current_time
|
||||
}
|
||||
|
||||
challenges.append(challenge)
|
||||
pool["challenges"] = challenges
|
||||
self._save_game_pool(chat_id, pool)
|
||||
|
||||
text = f"## 🎯 五子棋挑战\n\n"
|
||||
text += f"<at user_id=\"{user_id}\"></at> 发起了五子棋挑战!\n\n"
|
||||
text += f"💡 想要应战吗?输入 `.gomoku accept` 接受挑战"
|
||||
|
||||
return text
|
||||
|
||||
def _accept_challenge(self, chat_id: int, user_id: int) -> str:
|
||||
"""接受挑战
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 接受者ID
|
||||
|
||||
Returns:
|
||||
提示消息
|
||||
"""
|
||||
# 获取游戏池
|
||||
pool = self._get_game_pool(chat_id)
|
||||
games = pool.get("games", [])
|
||||
challenges = pool.get("challenges", [])
|
||||
|
||||
if not challenges:
|
||||
return "⚠️ 当前没有挑战可以接受\n\n输入 `.gomoku challenge` 发起挑战"
|
||||
|
||||
# 检查用户是否已经在对战中
|
||||
user_game = self._find_user_game(chat_id, user_id)
|
||||
if user_game:
|
||||
return "⚠️ 你已经在对战中!\n\n输入 `.gomoku show` 查看棋盘"
|
||||
|
||||
# 获取最新的挑战
|
||||
challenge = challenges[-1]
|
||||
challenger_id = challenge["challenger_id"]
|
||||
|
||||
# 不能接受自己的挑战
|
||||
if challenger_id == user_id:
|
||||
return "❌ 不能接受自己的挑战!"
|
||||
|
||||
# 检查是否已达到最大并发数
|
||||
active_games = [g for g in games if g["status"] == "playing"]
|
||||
if len(active_games) >= self.max_concurrent_games:
|
||||
return f"⚠️ 当前聊天已有 {len(active_games)} 局对战,已达到最大并发数限制"
|
||||
|
||||
# 创建游戏
|
||||
current_time = int(time.time())
|
||||
game_id = f"p{challenger_id}_p{user_id}_{current_time}"
|
||||
|
||||
new_game = {
|
||||
"game_id": game_id,
|
||||
"player_black": challenger_id, # 挑战者执黑(先手)
|
||||
"player_white": user_id, # 接受者执白(后手)
|
||||
"current_player": challenger_id, # 黑方先手
|
||||
"board": logic.create_empty_board(),
|
||||
"status": "playing",
|
||||
"winner": None,
|
||||
"moves": [],
|
||||
"last_move": None,
|
||||
"created_at": current_time,
|
||||
"updated_at": current_time
|
||||
}
|
||||
|
||||
games.append(new_game)
|
||||
|
||||
# 移除已接受的挑战
|
||||
challenges.remove(challenge)
|
||||
|
||||
pool["games"] = games
|
||||
pool["challenges"] = challenges
|
||||
self._save_game_pool(chat_id, pool)
|
||||
|
||||
text = f"## ⚫ 五子棋对战开始!\n\n"
|
||||
text += f"**黑方(先手)**:<at user_id=\"{challenger_id}\"></at> ⚫\n\n"
|
||||
text += f"**白方(后手)**:<at user_id=\"{user_id}\"></at> ⚪\n\n"
|
||||
text += f"**轮到**:<at user_id=\"{challenger_id}\"></at> ⚫\n\n"
|
||||
text += "💡 提示:\n"
|
||||
text += "- 黑方有禁手规则(三三、四四、长连禁手)\n"
|
||||
text += "- 输入 `.gomoku A1` 在A1位置落子\n"
|
||||
text += "- 输入 `.gomoku show` 查看棋盘"
|
||||
|
||||
return text
|
||||
|
||||
def _cancel_challenge(self, chat_id: int, user_id: int) -> str:
|
||||
"""取消挑战
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
提示消息
|
||||
"""
|
||||
# 获取游戏池
|
||||
pool = self._get_game_pool(chat_id)
|
||||
challenges = pool.get("challenges", [])
|
||||
|
||||
# 查找用户的挑战
|
||||
user_challenge = None
|
||||
for challenge in challenges:
|
||||
if challenge["challenger_id"] == user_id:
|
||||
user_challenge = challenge
|
||||
break
|
||||
|
||||
if not user_challenge:
|
||||
return "⚠️ 你没有发起挑战"
|
||||
|
||||
# 移除挑战
|
||||
challenges.remove(user_challenge)
|
||||
pool["challenges"] = challenges
|
||||
self._save_game_pool(chat_id, pool)
|
||||
|
||||
return "✅ 已取消挑战"
|
||||
|
||||
def _make_move(self, chat_id: int, user_id: int, coord: str) -> str:
|
||||
"""落子
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
coord: 坐标字符串
|
||||
|
||||
Returns:
|
||||
结果消息
|
||||
"""
|
||||
# 查找用户的游戏
|
||||
game = self._find_user_game(chat_id, user_id)
|
||||
if not game:
|
||||
return "⚠️ 你当前没有进行中的对战\n\n输入 `.gomoku @对手` 开始新游戏"
|
||||
|
||||
# 检查是否轮到该用户
|
||||
if game["current_player"] != user_id:
|
||||
opponent_id = game["player_white"] if game["player_black"] == user_id else game["player_black"]
|
||||
return f"⚠️ 现在轮到 <at user_id=\"{opponent_id}\"></at> 落子"
|
||||
|
||||
# 解析坐标
|
||||
position = logic.parse_coord(coord)
|
||||
if position is None:
|
||||
return f"❌ 无效的坐标:{coord}\n\n坐标格式如:A1, O15"
|
||||
|
||||
row, col = position
|
||||
|
||||
# 检查位置是否已有棋子
|
||||
if game["board"][row][col] != 0:
|
||||
return f"❌ 位置 {coord.upper()} 已有棋子"
|
||||
|
||||
# 确定当前玩家颜色
|
||||
player = 1 if game["player_black"] == user_id else 2
|
||||
player_name = "黑方" if player == 1 else "白方"
|
||||
player_emoji = "⚫" if player == 1 else "⚪"
|
||||
|
||||
# 检查黑方禁手
|
||||
if player == 1:
|
||||
is_forbidden, forbidden_type = logic.check_forbidden(game["board"], row, col)
|
||||
if is_forbidden:
|
||||
text = f"## ❌ {forbidden_type}!\n\n"
|
||||
text += f"位置 {coord.upper()} 触发禁手,黑方判负!\n\n"
|
||||
text += f"**获胜者**:<at user_id=\"{game['player_white']}\"></at> ⚪ 白方\n\n"
|
||||
text += f"📊 战绩已更新"
|
||||
|
||||
# 更新战绩
|
||||
self.db.update_game_stats(game['player_white'], 'gomoku', win=True)
|
||||
self.db.update_game_stats(game['player_black'], 'gomoku', loss=True)
|
||||
|
||||
# 移除游戏
|
||||
pool = self._get_game_pool(chat_id)
|
||||
pool["games"] = [g for g in pool["games"] if g["game_id"] != game["game_id"]]
|
||||
self._save_game_pool(chat_id, pool)
|
||||
|
||||
return text
|
||||
|
||||
# 落子
|
||||
game["board"][row][col] = player
|
||||
game["moves"].append((row, col, player))
|
||||
game["last_move"] = (row, col)
|
||||
game["updated_at"] = int(time.time())
|
||||
|
||||
# 检查是否获胜
|
||||
if logic.check_win(game["board"], row, col, player):
|
||||
text = f"## 🎉 五连珠!游戏结束!\n\n"
|
||||
text += f"**获胜者**:<at user_id=\"{user_id}\"></at> {player_emoji} {player_name}\n\n"
|
||||
|
||||
# 渲染棋盘
|
||||
board_str = logic.render_board(game["board"], game["last_move"])
|
||||
text += f"```\n{board_str}\n```\n\n"
|
||||
|
||||
text += f"📊 战绩已更新"
|
||||
|
||||
# 更新战绩
|
||||
opponent_id = game["player_white"] if player == 1 else game["player_black"]
|
||||
self.db.update_game_stats(user_id, 'gomoku', win=True)
|
||||
self.db.update_game_stats(opponent_id, 'gomoku', loss=True)
|
||||
|
||||
# 移除游戏
|
||||
pool = self._get_game_pool(chat_id)
|
||||
pool["games"] = [g for g in pool["games"] if g["game_id"] != game["game_id"]]
|
||||
self._save_game_pool(chat_id, pool)
|
||||
|
||||
return text
|
||||
|
||||
# 切换玩家
|
||||
opponent_id = game["player_white"] if player == 1 else game["player_black"]
|
||||
game["current_player"] = opponent_id
|
||||
opponent_emoji = "⚪" if player == 1 else "⚫"
|
||||
opponent_name = "白方" if player == 1 else "黑方"
|
||||
|
||||
# 更新游戏池
|
||||
pool = self._get_game_pool(chat_id)
|
||||
for i, g in enumerate(pool["games"]):
|
||||
if g["game_id"] == game["game_id"]:
|
||||
pool["games"][i] = game
|
||||
break
|
||||
self._save_game_pool(chat_id, pool)
|
||||
|
||||
# 渲染棋盘
|
||||
board_str = logic.render_board(game["board"], game["last_move"])
|
||||
|
||||
text = f"## ✅ 落子成功!\n\n"
|
||||
text += f"**位置**:{coord.upper()} {player_emoji}\n\n"
|
||||
text += f"**轮到**:<at user_id=\"{opponent_id}\"></at> {opponent_emoji} {opponent_name}\n\n"
|
||||
text += f"```\n{board_str}\n```"
|
||||
|
||||
return text
|
||||
|
||||
def _show_board(self, chat_id: int, user_id: int) -> str:
|
||||
"""显示棋盘
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
棋盘显示
|
||||
"""
|
||||
game = self._find_user_game(chat_id, user_id)
|
||||
if not game:
|
||||
return "⚠️ 你当前没有进行中的对战\n\n输入 `.gomoku @对手` 开始新游戏"
|
||||
|
||||
# 渲染棋盘
|
||||
board_str = logic.render_board(game["board"], game["last_move"])
|
||||
|
||||
# 获取当前玩家信息
|
||||
current_id = game["current_player"]
|
||||
current_emoji = "⚫" if game["player_black"] == current_id else "⚪"
|
||||
current_name = "黑方" if game["player_black"] == current_id else "白方"
|
||||
|
||||
text = f"## ⚫ 五子棋对战\n\n"
|
||||
text += f"**黑方**:<at user_id=\"{game['player_black']}\"></at> ⚫\n\n"
|
||||
text += f"**白方**:<at user_id=\"{game['player_white']}\"></at> ⚪\n\n"
|
||||
text += f"**轮到**:<at user_id=\"{current_id}\"></at> {current_emoji} {current_name}\n\n"
|
||||
text += f"**手数**:{len(game['moves'])}\n\n"
|
||||
text += f"```\n{board_str}\n```"
|
||||
|
||||
return text
|
||||
|
||||
def _resign(self, chat_id: int, user_id: int) -> str:
|
||||
"""认输
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
结果消息
|
||||
"""
|
||||
game = self._find_user_game(chat_id, user_id)
|
||||
if not game:
|
||||
return "⚠️ 你当前没有进行中的对战"
|
||||
|
||||
# 确定胜者
|
||||
if game["player_black"] == user_id:
|
||||
winner_id = game["player_white"]
|
||||
loser_name = "黑方"
|
||||
winner_emoji = "⚪"
|
||||
else:
|
||||
winner_id = game["player_black"]
|
||||
loser_name = "白方"
|
||||
winner_emoji = "⚫"
|
||||
|
||||
text = f"## 🏳️ 认输\n\n"
|
||||
text += f"<at user_id=\"{user_id}\"></at> {loser_name} 认输\n\n"
|
||||
text += f"**获胜者**:<at user_id=\"{winner_id}\"></at> {winner_emoji}\n\n"
|
||||
text += f"📊 战绩已更新"
|
||||
|
||||
# 更新战绩
|
||||
self.db.update_game_stats(winner_id, 'gomoku', win=True)
|
||||
self.db.update_game_stats(user_id, 'gomoku', loss=True)
|
||||
|
||||
# 移除游戏
|
||||
pool = self._get_game_pool(chat_id)
|
||||
pool["games"] = [g for g in pool["games"] if g["game_id"] != game["game_id"]]
|
||||
self._save_game_pool(chat_id, pool)
|
||||
|
||||
return text
|
||||
|
||||
def _list_games(self, chat_id: int) -> str:
|
||||
"""列出所有进行中的游戏
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
|
||||
Returns:
|
||||
游戏列表
|
||||
"""
|
||||
pool = self._get_game_pool(chat_id)
|
||||
active_games = [g for g in pool.get("games", []) if g["status"] == "playing"]
|
||||
|
||||
if not active_games:
|
||||
return "📋 当前没有进行中的对战\n\n输入 `.gomoku @对手` 开始新游戏"
|
||||
|
||||
text = f"## 📋 进行中的对战 ({len(active_games)}/{self.max_concurrent_games})\n\n"
|
||||
|
||||
for idx, game in enumerate(active_games, 1):
|
||||
current_emoji = "⚫" if game["player_black"] == game["current_player"] else "⚪"
|
||||
text += f"### {idx}. 对战\n"
|
||||
text += f"- **黑方**:<at user_id=\"{game['player_black']}\"></at> ⚫\n"
|
||||
text += f"- **白方**:<at user_id=\"{game['player_white']}\"></at> ⚪\n"
|
||||
text += f"- **轮到**:<at user_id=\"{game['current_player']}\"></at> {current_emoji}\n"
|
||||
text += f"- **手数**:{len(game['moves'])}\n\n"
|
||||
|
||||
return text
|
||||
|
||||
def _get_stats(self, user_id: int) -> str:
|
||||
"""获取用户战绩
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
战绩信息
|
||||
"""
|
||||
stats = self.db.get_game_stats(user_id, 'gomoku')
|
||||
|
||||
total = stats['total_plays']
|
||||
if total == 0:
|
||||
return "📊 你还没有五子棋对战记录\n\n快来挑战吧!输入 `.gomoku @对手` 开始游戏"
|
||||
|
||||
wins = stats['wins']
|
||||
losses = stats['losses']
|
||||
win_rate = (wins / total * 100) if total > 0 else 0
|
||||
|
||||
text = f"## 📊 五子棋战绩\n\n"
|
||||
text += f"**总局数**:{total} 局\n\n"
|
||||
text += f"**胜利**:{wins} 次 🎉\n\n"
|
||||
text += f"**失败**:{losses} 次\n\n"
|
||||
text += f"**胜率**:<font color='#4CAF50'>{win_rate:.1f}%</font>"
|
||||
|
||||
return text
|
||||
|
||||
def get_help(self) -> str:
|
||||
"""获取帮助信息"""
|
||||
return """## ⚫ 五子棋
|
||||
|
||||
### 基础用法
|
||||
- `.gomoku challenge` - 发起挑战
|
||||
- `.gomoku accept` - 接受挑战
|
||||
- `.gomoku A1` - 在A1位置落子
|
||||
- `.gomoku show` - 显示当前棋盘
|
||||
- `.gomoku resign` - 认输
|
||||
|
||||
### 其他指令
|
||||
- `.gomoku cancel` - 取消自己的挑战
|
||||
- `.gomoku list` - 列出所有进行中的对战
|
||||
- `.gomoku stats` - 查看个人战绩
|
||||
|
||||
### 游戏规则
|
||||
- 标准15×15棋盘,五子连珠获胜
|
||||
- 黑方先手,但有禁手规则:
|
||||
- **三三禁手**:一手棋同时形成两个活三
|
||||
- **四四禁手**:一手棋同时形成两个四(活四或冲四)
|
||||
- **长连禁手**:一手棋形成六子或以上连珠
|
||||
- 触发禁手者判负
|
||||
- 允许多轮对战同时进行(对战双方不同即可)
|
||||
|
||||
### 坐标系统
|
||||
- 列:A-O(15列)
|
||||
- 行:1-15(15行)
|
||||
- 示例:A1(左上角)、O15(右下角)、H8(中心)
|
||||
|
||||
### 示例
|
||||
```
|
||||
.gomoku challenge # 发起挑战
|
||||
.gomoku accept # 接受挑战
|
||||
.gomoku H8 # 在中心位置落子
|
||||
.gomoku show # 查看棋盘
|
||||
.gomoku resign # 认输
|
||||
```
|
||||
|
||||
💡 提示:黑方虽然先手,但需要注意禁手规则
|
||||
"""
|
||||
|
||||
287
games/gomoku_logic.py
Normal file
287
games/gomoku_logic.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""五子棋游戏逻辑模块"""
|
||||
from typing import Optional, Tuple, List, Dict, Any
|
||||
|
||||
|
||||
def create_empty_board() -> List[List[int]]:
|
||||
"""创建空棋盘
|
||||
|
||||
Returns:
|
||||
15x15的二维列表,0表示空位
|
||||
"""
|
||||
return [[0] * 15 for _ in range(15)]
|
||||
|
||||
|
||||
def parse_coord(coord_str: str) -> Optional[Tuple[int, int]]:
|
||||
"""解析坐标字符串
|
||||
|
||||
Args:
|
||||
coord_str: 如 "A1", "O15", "h8"
|
||||
|
||||
Returns:
|
||||
(row, col) 或 None
|
||||
"""
|
||||
coord_str = coord_str.strip().upper()
|
||||
|
||||
if len(coord_str) < 2:
|
||||
return None
|
||||
|
||||
# 解析列(A-O)
|
||||
col_char = coord_str[0]
|
||||
if not ('A' <= col_char <= 'O'):
|
||||
return None
|
||||
col = ord(col_char) - ord('A')
|
||||
|
||||
# 解析行(1-15)
|
||||
try:
|
||||
row = int(coord_str[1:]) - 1
|
||||
if not (0 <= row <= 14):
|
||||
return None
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
return (row, col)
|
||||
|
||||
|
||||
def format_coord(row: int, col: int) -> str:
|
||||
"""格式化坐标
|
||||
|
||||
Args:
|
||||
row: 0-14
|
||||
col: 0-14
|
||||
|
||||
Returns:
|
||||
如 "A1", "O15"
|
||||
"""
|
||||
col_char = chr(ord('A') + col)
|
||||
row_num = row + 1
|
||||
return f"{col_char}{row_num}"
|
||||
|
||||
|
||||
def is_valid_position(row: int, col: int) -> bool:
|
||||
"""检查坐标是否在棋盘范围内
|
||||
|
||||
Args:
|
||||
row: 行号
|
||||
col: 列号
|
||||
|
||||
Returns:
|
||||
是否有效
|
||||
"""
|
||||
return 0 <= row <= 14 and 0 <= col <= 14
|
||||
|
||||
|
||||
def count_consecutive(board: List[List[int]], row: int, col: int,
|
||||
direction: Tuple[int, int], player: int) -> int:
|
||||
"""统计某方向连续同色棋子数(包括当前位置)
|
||||
|
||||
Args:
|
||||
board: 棋盘状态
|
||||
row, col: 起始位置
|
||||
direction: 方向向量 (dr, dc)
|
||||
player: 玩家 (1:黑, 2:白)
|
||||
|
||||
Returns:
|
||||
连续棋子数
|
||||
"""
|
||||
dr, dc = direction
|
||||
count = 1 # 包括当前位置
|
||||
|
||||
# 正方向
|
||||
r, c = row + dr, col + dc
|
||||
while is_valid_position(r, c) and board[r][c] == player:
|
||||
count += 1
|
||||
r += dr
|
||||
c += dc
|
||||
|
||||
# 反方向
|
||||
r, c = row - dr, col - dc
|
||||
while is_valid_position(r, c) and board[r][c] == player:
|
||||
count += 1
|
||||
r -= dr
|
||||
c -= dc
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def check_win(board: List[List[int]], row: int, col: int, player: int) -> bool:
|
||||
"""检查是否获胜(恰好五连珠)
|
||||
|
||||
Args:
|
||||
board: 棋盘状态
|
||||
row, col: 最后落子位置
|
||||
player: 玩家 (1:黑, 2:白)
|
||||
|
||||
Returns:
|
||||
是否五连珠获胜
|
||||
"""
|
||||
# 四个方向:横、竖、左斜、右斜
|
||||
directions = [(0, 1), (1, 0), (1, 1), (1, -1)]
|
||||
|
||||
for direction in directions:
|
||||
count = count_consecutive(board, row, col, direction, player)
|
||||
if count == 5:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def analyze_line(board: List[List[int]], row: int, col: int,
|
||||
direction: Tuple[int, int], player: int) -> Dict[str, Any]:
|
||||
"""分析某方向的棋型
|
||||
|
||||
Args:
|
||||
board: 棋盘状态
|
||||
row, col: 待分析位置(假设已落子)
|
||||
direction: 方向向量
|
||||
player: 玩家
|
||||
|
||||
Returns:
|
||||
{
|
||||
"consecutive": int, # 连续数
|
||||
"left_open": bool, # 左侧是否开放
|
||||
"right_open": bool, # 右侧是否开放
|
||||
"pattern": str # 棋型类型
|
||||
}
|
||||
"""
|
||||
dr, dc = direction
|
||||
|
||||
# 统计正方向连续数
|
||||
right_count = 0
|
||||
r, c = row + dr, col + dc
|
||||
while is_valid_position(r, c) and board[r][c] == player:
|
||||
right_count += 1
|
||||
r += dr
|
||||
c += dc
|
||||
right_open = is_valid_position(r, c) and board[r][c] == 0
|
||||
|
||||
# 统计反方向连续数
|
||||
left_count = 0
|
||||
r, c = row - dr, col - dc
|
||||
while is_valid_position(r, c) and board[r][c] == player:
|
||||
left_count += 1
|
||||
r -= dr
|
||||
c -= dc
|
||||
left_open = is_valid_position(r, c) and board[r][c] == 0
|
||||
|
||||
# 总连续数(包括当前位置)
|
||||
consecutive = left_count + 1 + right_count
|
||||
|
||||
# 判定棋型
|
||||
pattern = "none"
|
||||
|
||||
if consecutive >= 6:
|
||||
pattern = "overline"
|
||||
elif consecutive == 5:
|
||||
pattern = "five"
|
||||
elif consecutive == 4:
|
||||
if left_open and right_open:
|
||||
pattern = "live_four"
|
||||
elif left_open or right_open:
|
||||
pattern = "rush_four"
|
||||
elif consecutive == 3:
|
||||
if left_open and right_open:
|
||||
pattern = "live_three"
|
||||
elif left_open or right_open:
|
||||
pattern = "sleep_three"
|
||||
|
||||
return {
|
||||
"consecutive": consecutive,
|
||||
"left_open": left_open,
|
||||
"right_open": right_open,
|
||||
"pattern": pattern
|
||||
}
|
||||
|
||||
|
||||
def check_forbidden(board: List[List[int]], row: int, col: int) -> Tuple[bool, str]:
|
||||
"""检查黑方禁手
|
||||
|
||||
Args:
|
||||
board: 棋盘状态(不包含待落子)
|
||||
row, col: 待落子位置
|
||||
|
||||
Returns:
|
||||
(是否禁手, 禁手类型)
|
||||
"""
|
||||
# 只有黑方(玩家1)有禁手
|
||||
player = 1
|
||||
|
||||
# 临时落子
|
||||
original_value = board[row][col]
|
||||
board[row][col] = player
|
||||
|
||||
# 四个方向
|
||||
directions = [(0, 1), (1, 0), (1, 1), (1, -1)]
|
||||
|
||||
live_threes = 0
|
||||
fours = 0
|
||||
has_overline = False
|
||||
|
||||
for direction in directions:
|
||||
analysis = analyze_line(board, row, col, direction, player)
|
||||
|
||||
if analysis["pattern"] == "overline":
|
||||
has_overline = True
|
||||
elif analysis["pattern"] == "live_three":
|
||||
live_threes += 1
|
||||
elif analysis["pattern"] in ["live_four", "rush_four"]:
|
||||
fours += 1
|
||||
|
||||
# 恢复棋盘
|
||||
board[row][col] = original_value
|
||||
|
||||
# 判定禁手
|
||||
if has_overline:
|
||||
return True, "长连禁手"
|
||||
if live_threes >= 2:
|
||||
return True, "三三禁手"
|
||||
if fours >= 2:
|
||||
return True, "四四禁手"
|
||||
|
||||
return False, ""
|
||||
|
||||
|
||||
def render_board(board: List[List[int]], last_move: Optional[Tuple[int, int]] = None) -> str:
|
||||
"""渲染棋盘为字符串
|
||||
|
||||
Args:
|
||||
board: 棋盘状态
|
||||
last_move: 最后落子位置(可选,用于标记)
|
||||
|
||||
Returns:
|
||||
棋盘的字符串表示
|
||||
"""
|
||||
lines = []
|
||||
|
||||
# 列标题 - 使用全角空格确保对齐
|
||||
col_labels = "\t " + " ".join([chr(ord('A') + i) + "" for i in range(15)])
|
||||
lines.append(col_labels.rstrip())
|
||||
|
||||
# 绘制棋盘
|
||||
for row in range(15):
|
||||
row_num = f"{row + 1:2d}" # 右对齐行号
|
||||
row_cells = []
|
||||
|
||||
for col in range(15):
|
||||
cell = board[row][col]
|
||||
|
||||
# 标记最后落子
|
||||
if last_move and last_move == (row, col):
|
||||
if cell == 1:
|
||||
row_cells.append("⚫")
|
||||
elif cell == 2:
|
||||
row_cells.append("⚪")
|
||||
else:
|
||||
row_cells.append("➕")
|
||||
else:
|
||||
if cell == 0:
|
||||
row_cells.append("➕")
|
||||
elif cell == 1:
|
||||
row_cells.append("⚫")
|
||||
elif cell == 2:
|
||||
row_cells.append("⚪")
|
||||
|
||||
# 每个emoji后面加一个空格
|
||||
lines.append(f"{row_num} " + "".join([cell + " " for cell in row_cells]).rstrip())
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
670
games/idiom.py
Normal file
670
games/idiom.py
Normal file
@@ -0,0 +1,670 @@
|
||||
"""成语接龙游戏"""
|
||||
import json
|
||||
import random
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Dict, Any, List
|
||||
from pypinyin import pinyin, Style
|
||||
from games.base import BaseGame
|
||||
from utils.parser import CommandParser
|
||||
from config import GAME_CONFIG
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IdiomGame(BaseGame):
|
||||
"""成语接龙游戏"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化游戏"""
|
||||
super().__init__()
|
||||
self.config = GAME_CONFIG.get('idiom', {})
|
||||
self.max_history_display = self.config.get('max_history_display', 10)
|
||||
self.starter_idioms = self.config.get('starter_idioms', [
|
||||
"一马当先", "龙马精神", "马到成功", "开门见山"
|
||||
])
|
||||
self._blacklist = None
|
||||
self.blacklist_file = Path(__file__).parent.parent / "data" / "idiom_blacklist.json"
|
||||
|
||||
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理成语接龙指令
|
||||
|
||||
Args:
|
||||
command: 指令,如 ".idiom start" 或 ".idiom 马到成功"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
回复消息
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
# 没有参数,显示帮助
|
||||
if not args:
|
||||
return self.get_help()
|
||||
|
||||
# 解析参数
|
||||
parts = args.split(maxsplit=1)
|
||||
action = parts[0].lower()
|
||||
|
||||
# 开始游戏
|
||||
if action in ['start', '开始']:
|
||||
starter = parts[1].strip() if len(parts) > 1 else None
|
||||
return self._start_game(chat_id, user_id, starter)
|
||||
|
||||
# 结束游戏
|
||||
if action in ['stop', '结束', 'end']:
|
||||
return self._stop_game(chat_id, user_id)
|
||||
|
||||
# 查看状态
|
||||
if action in ['status', '状态']:
|
||||
return self._show_status(chat_id)
|
||||
|
||||
# 查看黑名单
|
||||
if action in ['blacklist', '黑名单']:
|
||||
return self._show_blacklist(chat_id)
|
||||
|
||||
# 查看帮助
|
||||
if action in ['help', '帮助']:
|
||||
return self.get_help()
|
||||
|
||||
# 裁判拒绝
|
||||
if action in ['reject', '拒绝']:
|
||||
if len(parts) < 2:
|
||||
return "❌ 请指定要拒绝的词语,如:`.idiom reject 词语`"
|
||||
idiom_to_reject = parts[1].strip()
|
||||
return self._reject_idiom(chat_id, user_id, idiom_to_reject)
|
||||
|
||||
# 指定下一位
|
||||
if action in ['next', '下一位']:
|
||||
if len(parts) < 2:
|
||||
return "❌ 请@要指定的用户"
|
||||
mentioned = self._parse_mentioned_user(args)
|
||||
if not mentioned:
|
||||
return "❌ 未识别到@的用户"
|
||||
return self._set_next_user(chat_id, user_id, mentioned)
|
||||
|
||||
# 默认:接龙
|
||||
# 整个args都是成语(可能包含@用户)
|
||||
return self._make_chain(chat_id, user_id, args)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理成语接龙指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
def _load_blacklist(self) -> List[str]:
|
||||
"""懒加载全局黑名单
|
||||
|
||||
Returns:
|
||||
黑名单列表
|
||||
"""
|
||||
if self._blacklist is None:
|
||||
try:
|
||||
if self.blacklist_file.exists():
|
||||
with open(self.blacklist_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self._blacklist = data.get('blacklist', [])
|
||||
else:
|
||||
self._blacklist = []
|
||||
logger.info(f"黑名单加载完成,共 {len(self._blacklist)} 个词语")
|
||||
except Exception as e:
|
||||
logger.error(f"加载黑名单失败: {e}")
|
||||
self._blacklist = []
|
||||
return self._blacklist
|
||||
|
||||
def _save_blacklist(self):
|
||||
"""保存黑名单到文件"""
|
||||
try:
|
||||
self.blacklist_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = {
|
||||
"blacklist": self._blacklist if self._blacklist is not None else [],
|
||||
"description": "成语接龙游戏全局黑名单,被拒绝的词语将永久不可使用"
|
||||
}
|
||||
with open(self.blacklist_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"黑名单已保存,共 {len(data['blacklist'])} 个词语")
|
||||
except Exception as e:
|
||||
logger.error(f"保存黑名单失败: {e}")
|
||||
|
||||
def _get_pinyin(self, char: str, all_readings: bool = True) -> list:
|
||||
"""获取单字拼音
|
||||
|
||||
Args:
|
||||
char: 单个汉字
|
||||
all_readings: 是否返回多音字的所有读音
|
||||
|
||||
Returns:
|
||||
拼音列表
|
||||
"""
|
||||
try:
|
||||
result = pinyin(char, style=Style.NORMAL, heteronym=all_readings)
|
||||
return result[0] if result else []
|
||||
except Exception as e:
|
||||
logger.error(f"获取拼音错误: {e}")
|
||||
return []
|
||||
|
||||
def _check_pinyin_match(self, last_char: str, first_char: str) -> Tuple[bool, str, str]:
|
||||
"""检查拼音匹配
|
||||
|
||||
Args:
|
||||
last_char: 上一个成语的最后一个字
|
||||
first_char: 当前成语的第一个字
|
||||
|
||||
Returns:
|
||||
(是否匹配, 上一个拼音, 当前拼音)
|
||||
"""
|
||||
last_pinyins = self._get_pinyin(last_char)
|
||||
first_pinyins = self._get_pinyin(first_char)
|
||||
|
||||
# 只要有任何一个读音匹配就算成功
|
||||
for lp in last_pinyins:
|
||||
for fp in first_pinyins:
|
||||
if lp.lower() == fp.lower():
|
||||
return True, lp, fp
|
||||
|
||||
# 没有匹配,返回第一个读音
|
||||
return False, last_pinyins[0] if last_pinyins else '', first_pinyins[0] if first_pinyins else ''
|
||||
|
||||
def _parse_mentioned_user(self, content: str) -> Optional[int]:
|
||||
"""解析消息中@的用户ID
|
||||
|
||||
Args:
|
||||
content: 消息内容
|
||||
|
||||
Returns:
|
||||
用户ID或None
|
||||
"""
|
||||
# 简化实现:查找@后的数字
|
||||
# 实际WPS API可能有特定格式,需要根据文档调整
|
||||
match = re.search(r'@.*?(\d+)', content)
|
||||
if match:
|
||||
try:
|
||||
return int(match.group(1))
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _can_answer(self, state_data: Dict, user_id: int) -> Tuple[bool, str]:
|
||||
"""检查用户是否可以接龙
|
||||
|
||||
Args:
|
||||
state_data: 游戏状态数据
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
(是否可以, 错误消息)
|
||||
"""
|
||||
# 不能是上一个接龙的人
|
||||
if state_data.get('last_user_id') == user_id:
|
||||
return False, "❌ 不能连续接龙哦!让其他人来吧"
|
||||
|
||||
# 如果指定了下一位,必须是指定的人
|
||||
if state_data.get('next_user_id') is not None:
|
||||
if state_data['next_user_id'] != user_id:
|
||||
return False, f"❌ 现在轮到指定的人接龙了"
|
||||
|
||||
return True, ""
|
||||
|
||||
def _validate_idiom(self, idiom: str, state_data: Dict) -> Tuple[bool, str]:
|
||||
"""验证词语有效性
|
||||
|
||||
Args:
|
||||
idiom: 待验证的词语
|
||||
state_data: 游戏状态数据
|
||||
|
||||
Returns:
|
||||
(是否有效, 错误消息)
|
||||
"""
|
||||
# 检查长度
|
||||
if len(idiom) != 4:
|
||||
return False, "❌ 词语必须是4个字"
|
||||
|
||||
# 检查是否已使用
|
||||
if idiom in state_data.get('used_idioms', []):
|
||||
return False, f"❌ 「{idiom}」已经用过了"
|
||||
|
||||
# 检查是否在全局黑名单
|
||||
blacklist = self._load_blacklist()
|
||||
if idiom in blacklist:
|
||||
return False, f"❌ 「{idiom}」在黑名单中(永久禁用)"
|
||||
|
||||
# 检查拼音匹配
|
||||
current_idiom = state_data.get('current_idiom', '')
|
||||
if current_idiom:
|
||||
last_char = current_idiom[-1]
|
||||
first_char = idiom[0]
|
||||
is_match, last_py, first_py = self._check_pinyin_match(last_char, first_char)
|
||||
|
||||
if not is_match:
|
||||
return False, f"❌ 首字「{first_char}」拼音[{first_py}]不匹配上个成语尾字「{last_char}」拼音[{last_py}]"
|
||||
|
||||
return True, ""
|
||||
|
||||
def _start_game(self, chat_id: int, user_id: int, starter_idiom: Optional[str]) -> str:
|
||||
"""开始新游戏
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
starter_idiom: 起始成语(可选)
|
||||
|
||||
Returns:
|
||||
提示消息
|
||||
"""
|
||||
# 检查是否已有进行中的游戏(user_id=0表示群级别状态)
|
||||
state = self.db.get_game_state(chat_id, 0, 'idiom')
|
||||
if state:
|
||||
return "⚠️ 已经有一个进行中的游戏了!\n\n输入 `.idiom stop` 结束当前游戏"
|
||||
|
||||
# 确定起始成语
|
||||
if starter_idiom:
|
||||
# 验证起始成语
|
||||
if len(starter_idiom) != 4:
|
||||
return "❌ 起始成语必须是4个字"
|
||||
idiom = starter_idiom
|
||||
else:
|
||||
# 随机选择
|
||||
idiom = random.choice(self.starter_idioms)
|
||||
|
||||
# 获取最后一个字的拼音
|
||||
last_char = idiom[-1]
|
||||
last_pinyin_list = self._get_pinyin(last_char)
|
||||
last_pinyin = last_pinyin_list[0] if last_pinyin_list else ''
|
||||
|
||||
# 创建游戏状态
|
||||
state_data = {
|
||||
'creator_id': user_id,
|
||||
'current_idiom': idiom,
|
||||
'current_pinyin_last': last_pinyin,
|
||||
'last_user_id': user_id, # 发起人可以接第一个
|
||||
'next_user_id': None,
|
||||
'used_idioms': [idiom],
|
||||
'chain_length': 1,
|
||||
'participants': {},
|
||||
'history': [
|
||||
{
|
||||
'user_id': user_id,
|
||||
'idiom': idiom,
|
||||
'timestamp': int(time.time())
|
||||
}
|
||||
],
|
||||
'status': 'playing'
|
||||
}
|
||||
|
||||
# 保存群状态(user_id=0)
|
||||
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
|
||||
|
||||
text = f"## 🀄 成语接龙开始!\n\n"
|
||||
text += f"**起始成语**:{idiom} [{last_pinyin}]\n\n"
|
||||
text += f"任何人都可以接龙,输入 `.idiom [成语]` 开始吧!\n\n"
|
||||
text += f"💡 提示:可以用 `.idiom [成语] @某人` 指定下一位"
|
||||
|
||||
return text
|
||||
|
||||
def _make_chain(self, chat_id: int, user_id: int, args: str) -> str:
|
||||
"""玩家接龙
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
args: 参数(成语 + 可能的@用户)
|
||||
|
||||
Returns:
|
||||
结果消息
|
||||
"""
|
||||
# 获取群状态
|
||||
state = self.db.get_game_state(chat_id, 0, 'idiom')
|
||||
if not state:
|
||||
return "⚠️ 还没有开始游戏呢!\n\n输入 `.idiom start` 开始游戏"
|
||||
|
||||
state_data = state['state_data']
|
||||
|
||||
# 检查用户权限
|
||||
can_answer, error_msg = self._can_answer(state_data, user_id)
|
||||
if not can_answer:
|
||||
return error_msg
|
||||
|
||||
# 解析成语和@用户
|
||||
# 提取成语(去除@部分)
|
||||
idiom_match = re.match(r'^([^\s@]+)', args)
|
||||
if not idiom_match:
|
||||
return "❌ 请输入4个字的词语"
|
||||
|
||||
idiom = idiom_match.group(1).strip()
|
||||
|
||||
# 验证词语
|
||||
is_valid, error_msg = self._validate_idiom(idiom, state_data)
|
||||
if not is_valid:
|
||||
return error_msg
|
||||
|
||||
# 解析@用户
|
||||
mentioned_user_id = self._parse_mentioned_user(args)
|
||||
|
||||
# 获取拼音
|
||||
last_char = idiom[-1]
|
||||
last_pinyin_list = self._get_pinyin(last_char)
|
||||
last_pinyin = last_pinyin_list[0] if last_pinyin_list else ''
|
||||
|
||||
# 更新状态
|
||||
state_data['current_idiom'] = idiom
|
||||
state_data['current_pinyin_last'] = last_pinyin
|
||||
state_data['last_user_id'] = user_id
|
||||
state_data['next_user_id'] = mentioned_user_id
|
||||
state_data['used_idioms'].append(idiom)
|
||||
state_data['chain_length'] += 1
|
||||
|
||||
# 更新参与者统计
|
||||
if str(user_id) not in state_data['participants']:
|
||||
state_data['participants'][str(user_id)] = 0
|
||||
state_data['participants'][str(user_id)] += 1
|
||||
|
||||
# 记录历史
|
||||
state_data['history'].append({
|
||||
'user_id': user_id,
|
||||
'idiom': idiom,
|
||||
'timestamp': int(time.time())
|
||||
})
|
||||
|
||||
# 保存状态
|
||||
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
|
||||
|
||||
# 构建回复
|
||||
text = f"## ✅ 接龙成功!\n\n"
|
||||
text += f"**{idiom}** [{last_pinyin}]\n\n"
|
||||
text += f"**当前链长**:{state_data['chain_length']}\n\n"
|
||||
|
||||
user_count = state_data['participants'][str(user_id)]
|
||||
user_display_name = self.db.get_user_display_name(user_id)
|
||||
text += f"@{user_display_name} 成功次数:{user_count}\n\n"
|
||||
|
||||
if mentioned_user_id:
|
||||
mentioned_display_name = self.db.get_user_display_name(mentioned_user_id)
|
||||
text += f"已指定 @{mentioned_display_name} 接龙\n\n"
|
||||
else:
|
||||
text += "任何人都可以接龙\n\n"
|
||||
|
||||
text += "继续加油!💪"
|
||||
|
||||
return text
|
||||
|
||||
def _set_next_user(self, chat_id: int, user_id: int, next_user_id: int) -> str:
|
||||
"""指定下一位接龙者
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 当前用户ID
|
||||
next_user_id: 指定的下一位用户ID
|
||||
|
||||
Returns:
|
||||
提示消息
|
||||
"""
|
||||
# 获取群状态
|
||||
state = self.db.get_game_state(chat_id, 0, 'idiom')
|
||||
if not state:
|
||||
return "⚠️ 还没有开始游戏呢!"
|
||||
|
||||
state_data = state['state_data']
|
||||
|
||||
# 检查是否是最后接龙的人
|
||||
if state_data.get('last_user_id') != user_id:
|
||||
return "❌ 只有最后接龙成功的人可以指定下一位"
|
||||
|
||||
# 更新状态
|
||||
state_data['next_user_id'] = next_user_id
|
||||
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
|
||||
|
||||
next_user_display_name = self.db.get_user_display_name(next_user_id)
|
||||
return f"✅ 已指定 @{next_user_display_name} 接龙"
|
||||
|
||||
def _reject_idiom(self, chat_id: int, user_id: int, idiom: str) -> str:
|
||||
"""裁判拒绝词语
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
idiom: 要拒绝的词语
|
||||
|
||||
Returns:
|
||||
提示消息
|
||||
"""
|
||||
# 获取群状态
|
||||
state = self.db.get_game_state(chat_id, 0, 'idiom')
|
||||
if not state:
|
||||
return "⚠️ 还没有开始游戏呢!"
|
||||
|
||||
state_data = state['state_data']
|
||||
|
||||
# 检查权限(仅发起人)
|
||||
if state_data.get('creator_id') != user_id:
|
||||
return "❌ 只有游戏发起人可以执行裁判操作"
|
||||
|
||||
# 添加到全局黑名单
|
||||
blacklist = self._load_blacklist()
|
||||
if idiom not in blacklist:
|
||||
blacklist.append(idiom)
|
||||
self._save_blacklist()
|
||||
logger.info(f"词语「{idiom}」已加入全局黑名单")
|
||||
|
||||
# 如果是最后一个成语,回退状态
|
||||
if state_data.get('current_idiom') == idiom and len(state_data['history']) > 1:
|
||||
# 移除最后一条历史
|
||||
removed = state_data['history'].pop()
|
||||
removed_user = str(removed['user_id'])
|
||||
|
||||
# 减少该用户的计数
|
||||
if removed_user in state_data['participants']:
|
||||
state_data['participants'][removed_user] -= 1
|
||||
if state_data['participants'][removed_user] <= 0:
|
||||
del state_data['participants'][removed_user]
|
||||
|
||||
# 恢复到上一个成语
|
||||
if state_data['history']:
|
||||
last_entry = state_data['history'][-1]
|
||||
last_idiom = last_entry['idiom']
|
||||
last_char = last_idiom[-1]
|
||||
last_pinyin_list = self._get_pinyin(last_char)
|
||||
|
||||
state_data['current_idiom'] = last_idiom
|
||||
state_data['current_pinyin_last'] = last_pinyin_list[0] if last_pinyin_list else ''
|
||||
state_data['last_user_id'] = last_entry['user_id']
|
||||
state_data['next_user_id'] = None
|
||||
state_data['chain_length'] -= 1
|
||||
|
||||
# 从已使用列表中移除
|
||||
if idiom in state_data['used_idioms']:
|
||||
state_data['used_idioms'].remove(idiom)
|
||||
|
||||
# 保存状态
|
||||
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
|
||||
|
||||
text = f"✅ 已将「{idiom}」加入全局黑名单(永久禁用)"
|
||||
if state_data.get('current_idiom') != idiom:
|
||||
text += f"\n\n当前成语:{state_data['current_idiom']}"
|
||||
else:
|
||||
text += "\n\n游戏状态已回退"
|
||||
|
||||
return text
|
||||
|
||||
def _show_status(self, chat_id: int) -> str:
|
||||
"""显示游戏状态
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
|
||||
Returns:
|
||||
状态信息
|
||||
"""
|
||||
# 获取群状态
|
||||
state = self.db.get_game_state(chat_id, 0, 'idiom')
|
||||
if not state:
|
||||
return "⚠️ 还没有开始游戏呢!\n\n输入 `.idiom start` 开始游戏"
|
||||
|
||||
state_data = state['state_data']
|
||||
|
||||
text = f"## 🀄 成语接龙状态\n\n"
|
||||
text += f"**当前成语**:{state_data['current_idiom']} [{state_data['current_pinyin_last']}]\n\n"
|
||||
text += f"**链长**:{state_data['chain_length']}\n\n"
|
||||
|
||||
# 下一位
|
||||
if state_data.get('next_user_id'):
|
||||
next_user_display_name = self.db.get_user_display_name(state_data['next_user_id'])
|
||||
text += f"**下一位**:@{next_user_display_name}\n\n"
|
||||
else:
|
||||
text += f"**下一位**:任何人都可以接龙\n\n"
|
||||
|
||||
# 参与者排行
|
||||
if state_data['participants']:
|
||||
text += f"### 🏆 参与者排行\n\n"
|
||||
sorted_participants = sorted(
|
||||
state_data['participants'].items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True
|
||||
)
|
||||
for idx, (uid, count) in enumerate(sorted_participants[:5], 1):
|
||||
user_display_name = self.db.get_user_display_name(int(uid))
|
||||
text += f"{idx}. @{user_display_name} - {count}次\n"
|
||||
text += "\n"
|
||||
|
||||
# 最近成语
|
||||
history = state_data.get('history', [])
|
||||
if history:
|
||||
display_count = min(self.max_history_display, len(history))
|
||||
recent = history[-display_count:]
|
||||
text += f"### 📜 最近{display_count}个成语\n\n"
|
||||
text += " → ".join([h['idiom'] for h in recent])
|
||||
|
||||
return text
|
||||
|
||||
def _show_blacklist(self, chat_id: int) -> str:
|
||||
"""显示全局黑名单
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID(保留参数以保持接口一致性)
|
||||
|
||||
Returns:
|
||||
黑名单信息
|
||||
"""
|
||||
# 加载全局黑名单
|
||||
blacklist = self._load_blacklist()
|
||||
|
||||
if not blacklist:
|
||||
return "📋 全局黑名单为空\n\n💡 发起人可使用 `.idiom reject [词语]` 添加不合适的词语到黑名单"
|
||||
|
||||
text = f"## 📋 全局黑名单词语(永久禁用)\n\n"
|
||||
text += f"**共 {len(blacklist)} 个词语**\n\n"
|
||||
text += "、".join(blacklist)
|
||||
text += "\n\n💡 这些词语在所有游戏中都不可使用"
|
||||
|
||||
return text
|
||||
|
||||
def _stop_game(self, chat_id: int, user_id: int) -> str:
|
||||
"""结束游戏
|
||||
|
||||
Args:
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
总结消息
|
||||
"""
|
||||
# 获取群状态
|
||||
state = self.db.get_game_state(chat_id, 0, 'idiom')
|
||||
if not state:
|
||||
return "⚠️ 还没有开始游戏呢!"
|
||||
|
||||
state_data = state['state_data']
|
||||
|
||||
# 构建总结
|
||||
text = f"## 🎮 游戏结束!\n\n"
|
||||
text += f"**总链长**:{state_data['chain_length']}\n\n"
|
||||
text += f"**参与人数**:{len(state_data['participants'])}\n\n"
|
||||
|
||||
# 排行榜
|
||||
if state_data['participants']:
|
||||
text += f"### 🏆 排行榜\n\n"
|
||||
sorted_participants = sorted(
|
||||
state_data['participants'].items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True
|
||||
)
|
||||
for idx, (uid, count) in enumerate(sorted_participants, 1):
|
||||
user_display_name = self.db.get_user_display_name(int(uid))
|
||||
text += f"{idx}. @{user_display_name} - {count}次\n"
|
||||
# 更新统计
|
||||
try:
|
||||
for _ in range(count):
|
||||
self.db.update_game_stats(int(uid), 'idiom', win=True)
|
||||
except Exception as e:
|
||||
logger.error(f"更新统计失败: {e}")
|
||||
text += "\n"
|
||||
|
||||
# 完整接龙
|
||||
history = state_data.get('history', [])
|
||||
if history:
|
||||
text += f"### 📜 完整接龙\n\n"
|
||||
idioms = [h['idiom'] for h in history]
|
||||
text += " → ".join(idioms)
|
||||
|
||||
# 删除游戏状态
|
||||
self.db.delete_game_state(chat_id, 0, 'idiom')
|
||||
|
||||
return text
|
||||
|
||||
def _format_history(self, history: list, count: int) -> str:
|
||||
"""格式化历史记录
|
||||
|
||||
Args:
|
||||
history: 历史记录列表
|
||||
count: 显示数量
|
||||
|
||||
Returns:
|
||||
格式化的字符串
|
||||
"""
|
||||
if not history:
|
||||
return ""
|
||||
|
||||
display_count = min(count, len(history))
|
||||
recent = history[-display_count:]
|
||||
return " → ".join([h['idiom'] for h in recent])
|
||||
|
||||
def get_help(self) -> str:
|
||||
"""获取帮助信息"""
|
||||
return """## 🀄 成语接龙
|
||||
|
||||
### 基础用法
|
||||
- `.idiom start [成语]` - 开始游戏(可指定起始成语)
|
||||
- `.idiom [成语]` - 接龙
|
||||
- `.idiom [成语] @某人` - 接龙并指定下一位
|
||||
- `.idiom stop` - 结束游戏(任何人可执行)
|
||||
|
||||
### 其他指令
|
||||
- `.idiom status` - 查看游戏状态
|
||||
- `.idiom blacklist` - 查看黑名单
|
||||
- `.idiom reject [词语]` - 裁判拒绝词语(仅发起人)
|
||||
- `.idiom next @某人` - 指定下一位(仅最后接龙者)
|
||||
|
||||
### 游戏规则
|
||||
- 词语必须是4个字
|
||||
- 首字拼音必须匹配上个成语尾字拼音(忽略声调)
|
||||
- 不能重复使用成语
|
||||
- 不能连续接龙
|
||||
- 黑名单词语不可使用
|
||||
- 任何人都可以结束游戏
|
||||
|
||||
### 示例
|
||||
```
|
||||
.idiom start 一马当先 # 开始游戏
|
||||
.idiom 先声夺人 # 接龙
|
||||
.idiom 人山人海 @张三 # 接龙并指定下一位
|
||||
.idiom reject 某词 # 发起人拒绝某词
|
||||
.idiom stop # 结束游戏
|
||||
```
|
||||
|
||||
💡 提示:支持多音字和谐音接龙
|
||||
"""
|
||||
|
||||
184
games/points.py
Normal file
184
games/points.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""积分系统游戏模块"""
|
||||
import random
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from games.base import BaseGame
|
||||
from utils.parser import CommandParser
|
||||
from core.database import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PointsGame(BaseGame):
|
||||
"""积分系统游戏"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化游戏"""
|
||||
super().__init__()
|
||||
self.db = get_db()
|
||||
|
||||
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理积分相关指令
|
||||
|
||||
Args:
|
||||
command: 指令,如 ".points", ".checkin", ".leaderboard"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
回复消息
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip().lower()
|
||||
|
||||
# 积分排行榜
|
||||
if args in ['leaderboard', '排行榜', '排行']:
|
||||
return self._get_leaderboard()
|
||||
|
||||
|
||||
# 默认:每日签到
|
||||
else:
|
||||
return self._daily_checkin(user_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理积分指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
def _daily_checkin(self, user_id: int) -> str:
|
||||
"""每日签到
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
签到结果消息
|
||||
"""
|
||||
# 固定签到积分
|
||||
checkin_points = 100
|
||||
|
||||
# 检查是否已签到
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
if self.db.check_daily_checkin(user_id, today):
|
||||
return self._get_user_points(user_id)
|
||||
|
||||
# 执行签到
|
||||
try:
|
||||
result = self.db.daily_checkin(user_id, checkin_points)
|
||||
if result:
|
||||
# 获取用户积分信息
|
||||
points_info = self.db.get_user_points(user_id)
|
||||
|
||||
text = f"## ✅ 签到成功!\n\n"
|
||||
text += f"**获得积分**:+{checkin_points} 分\n\n"
|
||||
text += f"**当前积分**:{points_info['points']} 分\n\n"
|
||||
text += f"📅 签到日期:{today}\n\n"
|
||||
text += "💡 提示:每天签到可获得固定积分奖励!"
|
||||
|
||||
return text
|
||||
else:
|
||||
return "❌ 签到失败,请稍后重试"
|
||||
except Exception as e:
|
||||
logger.error(f"签到过程中发生错误: {e}", exc_info=True)
|
||||
return f"❌ 签到过程中发生错误: {str(e)}"
|
||||
|
||||
def _get_user_points(self, user_id: int) -> str:
|
||||
"""获取用户积分信息
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
积分信息消息
|
||||
"""
|
||||
points_info = self.db.get_user_points(user_id)
|
||||
|
||||
text = f"## 💎 个人积分\n\n"
|
||||
text += f"**当前积分**:{points_info['points']} 分\n\n"
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:\n"
|
||||
text += "• 每日签到可获得 100 积分\n"
|
||||
text += "• 查看运势可获得随机积分\n"
|
||||
text += "• 使用 `.points leaderboard` 查看排行榜"
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def _get_leaderboard(self, limit: int = 10) -> str:
|
||||
"""获取积分排行榜
|
||||
|
||||
Args:
|
||||
limit: 限制数量
|
||||
|
||||
Returns:
|
||||
排行榜消息
|
||||
"""
|
||||
leaderboard = self.db.get_points_leaderboard(limit)
|
||||
|
||||
if not leaderboard:
|
||||
return "📊 暂无排行榜数据"
|
||||
|
||||
text = f"## 🏆 积分排行榜(前 {len(leaderboard)} 名)\n\n"
|
||||
|
||||
medals = ["🥇", "🥈", "🥉"] + ["🏅"] * (limit - 3)
|
||||
|
||||
for i, user in enumerate(leaderboard):
|
||||
rank = i + 1
|
||||
medal = medals[i] if i < len(medals) else "🏅"
|
||||
username = self.db.get_user_display_name(user['user_id'])
|
||||
points = user.get('points', 0)
|
||||
|
||||
text += f"{medal} **第 {rank} 名** {username}\n"
|
||||
text += f" 积分:{points} 分\n\n"
|
||||
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:使用 `.points` 查看个人积分"
|
||||
|
||||
return text
|
||||
|
||||
def add_fortune_points(self, user_id: int) -> int:
|
||||
"""为运势游戏添加随机积分奖励
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
获得的积分数量
|
||||
"""
|
||||
# 随机积分范围:50-200分
|
||||
points = random.randint(50, 200)
|
||||
|
||||
if self.db.add_points(user_id, points, "fortune", "运势奖励"):
|
||||
logger.info(f"用户 {user_id} 通过运势获得 {points} 积分")
|
||||
return points
|
||||
else:
|
||||
logger.error(f"用户 {user_id} 运势积分奖励失败")
|
||||
return 0
|
||||
|
||||
def get_help(self) -> str:
|
||||
"""获取帮助信息"""
|
||||
return """## 💎 积分系统
|
||||
|
||||
### 基础用法
|
||||
- `.points` - 查看个人积分
|
||||
- `.checkin` - 每日签到
|
||||
- `.points leaderboard` - 积分排行榜
|
||||
|
||||
### 积分获取方式
|
||||
- **每日签到**:固定 10 积分
|
||||
- **运势占卜**:随机 1-20 积分
|
||||
- **游戏奖励**:根据游戏表现获得
|
||||
|
||||
### 示例
|
||||
```
|
||||
.points
|
||||
.checkin
|
||||
.points leaderboard
|
||||
```
|
||||
|
||||
### 说明
|
||||
- 每日签到只能进行一次
|
||||
- 运势积分每次查看都有机会获得
|
||||
- 积分系统已简化,不再保留历史记录
|
||||
"""
|
||||
1261
games/werewolf.py
Normal file
1261
games/werewolf.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,5 +15,12 @@ pydantic-settings==2.1.0
|
||||
# 系统监控
|
||||
psutil==7.1.2
|
||||
|
||||
# 拼音处理
|
||||
pypinyin==0.51.0
|
||||
|
||||
# AI对话框架
|
||||
llama-index-core>=0.10.0
|
||||
llama-index-llms-ollama>=0.1.0
|
||||
|
||||
# 注意:使用Python标准库sqlite3,不引入SQLAlchemy
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Callback路由处理"""
|
||||
import logging
|
||||
import re
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
@@ -28,7 +29,8 @@ async def callback_receive(request: Request):
|
||||
# 解析请求数据
|
||||
data = await request.json()
|
||||
logger.info(f"收到消息: chatid={data.get('chatid')}, creator={data.get('creator')}")
|
||||
logger.debug(f"消息内容: {data.get('content')}")
|
||||
logger.info(f"消息内容: {data.get('content')}")
|
||||
logger.info(f"完整callback数据: {data}")
|
||||
|
||||
# 验证请求
|
||||
try:
|
||||
@@ -47,6 +49,15 @@ async def callback_receive(request: Request):
|
||||
game_type, command = parse_result
|
||||
logger.info(f"识别指令: game_type={game_type}, command={command}")
|
||||
|
||||
# 检查是否包含 @s 参数(私聊标志)
|
||||
use_private_url = False
|
||||
# 使用正则表达式匹配独立的 @s 参数(前后有空格或字符串边界)
|
||||
if re.search(r'\s+@s\s+|\s+@s$|^@s\s+|^@s$', command):
|
||||
use_private_url = True
|
||||
# 从命令中移除 @s 参数,保持其他参数不变
|
||||
command = re.sub(r'\s+@s(\s+|$)|^@s\s+', ' ', command).strip()
|
||||
logger.info(f"检测到 @s 参数,将优先使用个人URL发送反馈,清理后的命令: {command}")
|
||||
|
||||
# 检查限流
|
||||
rate_limiter = get_rate_limiter()
|
||||
if not rate_limiter.is_allowed():
|
||||
@@ -74,15 +85,75 @@ async def callback_receive(request: Request):
|
||||
|
||||
# 发送回复
|
||||
if response_text:
|
||||
sender = get_message_sender()
|
||||
|
||||
# 根据内容选择消息类型
|
||||
if response_text.startswith('#'):
|
||||
# Markdown格式
|
||||
await sender.send_markdown(response_text)
|
||||
# 如果使用了 @s 参数,优先发送到个人URL
|
||||
if use_private_url:
|
||||
db = get_db()
|
||||
user_webhook_url = db.get_user_webhook_url(callback_data.creator)
|
||||
|
||||
if user_webhook_url:
|
||||
# 有个人URL,发送到个人URL
|
||||
from utils.message import send_private_message
|
||||
# 判断消息类型
|
||||
if game_type == 'ai_chat':
|
||||
msg_type = 'markdown'
|
||||
elif response_text.startswith('#'):
|
||||
msg_type = 'markdown'
|
||||
else:
|
||||
msg_type = 'text'
|
||||
|
||||
success = await send_private_message(
|
||||
user_id=callback_data.creator,
|
||||
content=response_text,
|
||||
msg_type=msg_type
|
||||
)
|
||||
if not success:
|
||||
# 如果私聊发送失败,回退到主URL
|
||||
logger.warning(f"个人URL发送失败,回退到主URL: user_id={callback_data.creator}")
|
||||
sender = get_message_sender()
|
||||
if game_type == 'ai_chat':
|
||||
try:
|
||||
await sender.send_markdown(response_text)
|
||||
except Exception as send_md_err:
|
||||
logger.error(f"发送Markdown消息失败,改用文本发送: {send_md_err}")
|
||||
await sender.send_text(response_text)
|
||||
else:
|
||||
if response_text.startswith('#'):
|
||||
await sender.send_markdown(response_text)
|
||||
else:
|
||||
await sender.send_text(response_text)
|
||||
# 成功发送到个人URL,不向主URL发送
|
||||
else:
|
||||
# 没有个人URL,回退到主URL
|
||||
logger.info(f"用户 {callback_data.creator} 没有注册个人URL,使用主URL发送")
|
||||
sender = get_message_sender()
|
||||
if game_type == 'ai_chat':
|
||||
try:
|
||||
await sender.send_markdown(response_text)
|
||||
except Exception as send_md_err:
|
||||
logger.error(f"发送Markdown消息失败,改用文本发送: {send_md_err}")
|
||||
await sender.send_text(response_text)
|
||||
else:
|
||||
if response_text.startswith('#'):
|
||||
await sender.send_markdown(response_text)
|
||||
else:
|
||||
await sender.send_text(response_text)
|
||||
else:
|
||||
# 普通文本
|
||||
await sender.send_text(response_text)
|
||||
# 没有 @s 参数,正常发送到主URL
|
||||
sender = get_message_sender()
|
||||
|
||||
# AI 对话:统一按 Markdown 发送(按任务决策)
|
||||
if game_type == 'ai_chat':
|
||||
try:
|
||||
await sender.send_markdown(response_text)
|
||||
except Exception as send_md_err:
|
||||
logger.error(f"发送Markdown消息失败,改用文本发送: {send_md_err}")
|
||||
await sender.send_text(response_text)
|
||||
else:
|
||||
# 其他模块保持原有启发式:以 # 开头视为 Markdown,否则文本
|
||||
if response_text.startswith('#'):
|
||||
await sender.send_markdown(response_text)
|
||||
else:
|
||||
await sender.send_text(response_text)
|
||||
|
||||
return JSONResponse({"result": "ok"})
|
||||
|
||||
@@ -116,6 +187,10 @@ async def handle_command(game_type: str, command: str,
|
||||
from games.base import get_stats_message
|
||||
return get_stats_message(user_id)
|
||||
|
||||
# 注册系统
|
||||
if game_type == 'register':
|
||||
return await handle_register_command(command, chat_id, user_id)
|
||||
|
||||
# 骰娘游戏
|
||||
if game_type == 'dice':
|
||||
from games.dice import DiceGame
|
||||
@@ -146,6 +221,73 @@ async def handle_command(game_type: str, command: str,
|
||||
game = QuizGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 成语接龙
|
||||
if game_type == 'idiom':
|
||||
from games.idiom import IdiomGame
|
||||
game = IdiomGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 五子棋
|
||||
if game_type == 'gomoku':
|
||||
from games.gomoku import GomokuGame
|
||||
game = GomokuGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 积分系统
|
||||
if game_type == 'points':
|
||||
from games.points import PointsGame
|
||||
game = PointsGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 炼金系统
|
||||
if game_type == 'alchemy':
|
||||
from games.alchemy import AlchemyGame
|
||||
game = AlchemyGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 冒险系统
|
||||
if game_type == 'adventure':
|
||||
from games.adventure import AdventureGame
|
||||
game = AdventureGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 积分赠送系统
|
||||
if game_type == 'gift':
|
||||
from games.gift import GiftGame
|
||||
game = GiftGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 复述功能
|
||||
if game_type == 'say':
|
||||
# 提取参数并原样返回
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
if not args:
|
||||
return "用法:.say 你想让我说的话\n别名:.说 / .复述"
|
||||
return args
|
||||
|
||||
# 私聊功能
|
||||
if game_type == 'talk':
|
||||
return await handle_talk_command(command, chat_id, user_id)
|
||||
|
||||
# AI对话系统
|
||||
if game_type == 'ai_chat':
|
||||
from games.ai_chat import AIChatGame
|
||||
game = AIChatGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 赌场系统
|
||||
if game_type == 'casino':
|
||||
from games.casino import CasinoGame
|
||||
game = CasinoGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 狼人杀系统
|
||||
if game_type == 'werewolf':
|
||||
from games.werewolf import WerewolfGame
|
||||
game = WerewolfGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 未知游戏类型
|
||||
logger.warning(f"未知游戏类型: {game_type}")
|
||||
return "❌ 未知的游戏类型"
|
||||
@@ -154,3 +296,133 @@ async def handle_command(game_type: str, command: str,
|
||||
logger.error(f"处理游戏指令异常: {e}", exc_info=True)
|
||||
return f"❌ 处理指令时出错: {str(e)}"
|
||||
|
||||
|
||||
async def handle_register_command(command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理注册命令
|
||||
|
||||
Args:
|
||||
command: 完整指令 ".register name" 或 ".register url <url>"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
注册结果消息
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
# 验证参数
|
||||
if not args:
|
||||
return "❌ 请提供要注册的内容!\n\n正确格式:\n`.register <名称>` - 注册用户名\n`.register url <URL>` - 注册webhook URL\n\n示例:\n`.register 张三`\n`.register url https://example.com/webhook?key=xxx`"
|
||||
|
||||
# 检查是否为url子命令
|
||||
parts = args.split(maxsplit=1)
|
||||
if parts and parts[0].lower() == 'url':
|
||||
# 处理URL注册
|
||||
if len(parts) < 2:
|
||||
return "❌ 请提供webhook URL!\n\n正确格式:`.register url <URL>`\n\n示例:\n`.register url https://example.com/webhook?key=xxx`"
|
||||
|
||||
webhook_url = parts[1].strip()
|
||||
|
||||
# URL验证
|
||||
if not webhook_url.startswith(('http://', 'https://')):
|
||||
return "❌ URL格式无效!必须以 http:// 或 https:// 开头。"
|
||||
|
||||
# 设置URL
|
||||
db = get_db()
|
||||
success = db.set_user_webhook_url(user_id, webhook_url)
|
||||
|
||||
if success:
|
||||
return f"✅ Webhook URL注册成功!\n\n**您的个人URL**:{webhook_url}\n\n私聊消息将发送到此URL。"
|
||||
else:
|
||||
return "❌ 注册失败!请稍后重试。"
|
||||
else:
|
||||
# 原有的名称注册逻辑
|
||||
if len(args) > 20:
|
||||
return "❌ 名称过长!最多支持20个字符。"
|
||||
|
||||
# 更新用户名称
|
||||
db = get_db()
|
||||
success = db.update_user_name(user_id, args)
|
||||
|
||||
if success:
|
||||
return f"✅ 注册成功!\n\n**您的名称**:{args}\n\n之后您可以使用这个名称参与各种游戏和功能。"
|
||||
else:
|
||||
return "❌ 注册失败!请稍后重试。"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理注册指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
|
||||
async def handle_talk_command(command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理私聊命令
|
||||
|
||||
Args:
|
||||
command: 完整指令 ".talk <username> <content>"
|
||||
chat_id: 会话ID
|
||||
user_id: 发送者用户ID
|
||||
|
||||
Returns:
|
||||
处理结果消息
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
# 验证参数
|
||||
if not args:
|
||||
return "❌ 请提供用户名和消息内容!\n\n正确格式:`.talk <用户名> <消息内容>`\n\n示例:\n`.talk 张三 你好,想和你聊聊`\n`.talk 李四 这是一条私聊消息`"
|
||||
|
||||
# 解析username和content(第一个单词是username,剩余部分是content)
|
||||
parts = args.split(maxsplit=1)
|
||||
if len(parts) < 2:
|
||||
return "❌ 请提供用户名和消息内容!\n\n正确格式:`.talk <用户名> <消息内容>`\n\n示例:\n`.talk 张三 你好,想和你聊聊`"
|
||||
|
||||
target_username = parts[0].strip()
|
||||
content = parts[1].strip()
|
||||
|
||||
if not target_username:
|
||||
return "❌ 用户名不能为空!\n\n正确格式:`.talk <用户名> <消息内容>`"
|
||||
if not content:
|
||||
return "❌ 消息内容不能为空!\n\n正确格式:`.talk <用户名> <消息内容>`"
|
||||
|
||||
# 通过用户名查找目标用户
|
||||
db = get_db()
|
||||
target_user = db.get_user_by_name(target_username)
|
||||
|
||||
if not target_user:
|
||||
return f"❌ 找不到用户名为「{target_username}」的用户!\n\n提示:目标用户需要使用 `.register <名称>` 注册用户名。"
|
||||
|
||||
target_user_id = target_user['user_id']
|
||||
|
||||
# 检查目标用户是否有注册名称(应该有,因为是通过名称找到的)
|
||||
if not target_user.get('username'):
|
||||
return f"❌ 用户「{target_username}」尚未注册用户名!"
|
||||
|
||||
# 检查目标用户是否有个人webhook URL
|
||||
if not db.has_webhook_url(target_user_id):
|
||||
return f"❌ 用户「{target_username}」尚未注册个人webhook URL!\n\n提示:目标用户需要使用 `.register url <URL>` 注册个人URL后才能接收私聊消息。"
|
||||
|
||||
# 发送私聊消息
|
||||
from utils.message import send_private_message
|
||||
success = await send_private_message(
|
||||
user_id=target_user_id,
|
||||
content=content,
|
||||
msg_type='text'
|
||||
)
|
||||
|
||||
if success:
|
||||
# 私聊消息发送成功,不向主URL发送提示消息
|
||||
return ""
|
||||
else:
|
||||
# 发送失败时仍然需要提示用户
|
||||
return f"❌ 发送私聊消息失败,请稍后重试。"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理私聊指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
|
||||
111
routers/private.py
Normal file
111
routers/private.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""私聊相关API路由"""
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from core.database import get_db
|
||||
from core.models import PrivateMessageRequest, CheckBatchRequest, CheckBatchResponse
|
||||
from utils.message import send_private_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/private/send")
|
||||
async def send_private(request: PrivateMessageRequest):
|
||||
"""发送私聊消息
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_id": 123456,
|
||||
"content": "消息内容",
|
||||
"msg_type": "text" // 可选,默认为"text"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# 验证msg_type
|
||||
if request.msg_type not in ['text', 'markdown']:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="msg_type必须是'text'或'markdown'"
|
||||
)
|
||||
|
||||
# 调用send_private_message
|
||||
success = await send_private_message(
|
||||
user_id=request.user_id,
|
||||
content=request.content,
|
||||
msg_type=request.msg_type
|
||||
)
|
||||
|
||||
if not success:
|
||||
# 检查用户是否有个人URL
|
||||
db = get_db()
|
||||
has_url = db.has_webhook_url(request.user_id)
|
||||
if not has_url:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"用户 {request.user_id} 没有注册个人webhook URL"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="消息发送失败,请稍后重试"
|
||||
)
|
||||
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"message": "消息发送成功"
|
||||
})
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"发送私聊消息API错误: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"服务器内部错误: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/private/check/{user_id}")
|
||||
async def check_user_webhook(user_id: int):
|
||||
"""检查用户是否有个人webhook URL"""
|
||||
try:
|
||||
db = get_db()
|
||||
has_webhook_url = db.has_webhook_url(user_id)
|
||||
|
||||
return JSONResponse({
|
||||
"user_id": user_id,
|
||||
"has_webhook_url": has_webhook_url
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"检查用户webhook URL错误: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"服务器内部错误: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/private/check-batch")
|
||||
async def check_users_webhook_batch(request: CheckBatchRequest):
|
||||
"""批量检查用户是否有个人webhook URL
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_ids": [123456, 789012, ...]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
db = get_db()
|
||||
results = db.check_users_webhook_urls(request.user_ids)
|
||||
|
||||
return CheckBatchResponse(results=results)
|
||||
except Exception as e:
|
||||
logger.error(f"批量检查用户webhook URL错误: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"服务器内部错误: {str(e)}"
|
||||
)
|
||||
|
||||
150
start.bat
Normal file
150
start.bat
Normal file
@@ -0,0 +1,150 @@
|
||||
@echo off
|
||||
REM WPS Bot Game Windows启动脚本
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM 默认配置
|
||||
set DEFAULT_WEBHOOK_URL=
|
||||
set DEFAULT_HOST=0.0.0.0
|
||||
set DEFAULT_PORT=11000
|
||||
set DEFAULT_WORKERS=1
|
||||
set DEFAULT_LOG_LEVEL=info
|
||||
|
||||
REM 显示帮助信息
|
||||
:show_help
|
||||
echo WPS Bot Game Windows启动脚本
|
||||
echo.
|
||||
echo 用法: %0 [选项]
|
||||
echo.
|
||||
echo 选项:
|
||||
echo -w, --webhook-url URL 设置WPS Webhook URL
|
||||
echo -H, --host HOST 服务器主机地址 (默认: %DEFAULT_HOST%)
|
||||
echo -p, --port PORT 服务器端口 (默认: %DEFAULT_PORT%)
|
||||
echo -W, --workers NUM 工作进程数 (默认: %DEFAULT_WORKERS%)
|
||||
echo -l, --log-level LEVEL 日志级别 (默认: %DEFAULT_LOG_LEVEL%)
|
||||
echo -h, --help 显示此帮助信息
|
||||
echo.
|
||||
echo 示例:
|
||||
echo %0 -w "https://xz.wps.cn/api/v1/webhook/send?key=your_key"
|
||||
echo %0 -w "https://xz.wps.cn/api/v1/webhook/send?key=your_key" -p 8080
|
||||
echo %0 --webhook-url "https://xz.wps.cn/api/v1/webhook/send?key=your_key" --port 8080 --log-level debug
|
||||
goto :eof
|
||||
|
||||
REM 初始化变量
|
||||
set WEBHOOK_URL=%DEFAULT_WEBHOOK_URL%
|
||||
set HOST=%DEFAULT_HOST%
|
||||
set PORT=%DEFAULT_PORT%
|
||||
set WORKERS=%DEFAULT_WORKERS%
|
||||
set LOG_LEVEL=%DEFAULT_LOG_LEVEL%
|
||||
|
||||
REM 解析命令行参数
|
||||
:parse_args
|
||||
if "%~1"=="" goto :start_app
|
||||
if "%~1"=="-w" (
|
||||
set WEBHOOK_URL=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="--webhook-url" (
|
||||
set WEBHOOK_URL=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="-H" (
|
||||
set HOST=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="--host" (
|
||||
set HOST=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="-p" (
|
||||
set PORT=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="--port" (
|
||||
set PORT=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="-W" (
|
||||
set WORKERS=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="--workers" (
|
||||
set WORKERS=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="-l" (
|
||||
set LOG_LEVEL=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="--log-level" (
|
||||
set LOG_LEVEL=%~2
|
||||
shift
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if "%~1"=="-h" (
|
||||
call :show_help
|
||||
exit /b 0
|
||||
)
|
||||
if "%~1"=="--help" (
|
||||
call :show_help
|
||||
exit /b 0
|
||||
)
|
||||
echo 未知参数: %~1
|
||||
call :show_help
|
||||
exit /b 1
|
||||
|
||||
:start_app
|
||||
REM 检查Python环境
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo 错误: 未找到Python
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 检查依赖
|
||||
python -c "import fastapi, uvicorn" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo 错误: 缺少必要的Python依赖
|
||||
echo 请运行: pip install -r requirements.txt
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 构建启动命令
|
||||
set CMD=python app.py --host %HOST% --port %PORT% --workers %WORKERS% --log-level %LOG_LEVEL%
|
||||
|
||||
if not "%WEBHOOK_URL%"=="" (
|
||||
set CMD=%CMD% --webhook-url "%WEBHOOK_URL%"
|
||||
)
|
||||
|
||||
REM 显示启动信息
|
||||
echo 启动WPS Bot Game...
|
||||
echo 主机: %HOST%
|
||||
echo 端口: %PORT%
|
||||
echo 工作进程: %WORKERS%
|
||||
echo 日志级别: %LOG_LEVEL%
|
||||
if not "%WEBHOOK_URL%"=="" (
|
||||
echo Webhook URL: %WEBHOOK_URL%
|
||||
)
|
||||
echo.
|
||||
|
||||
REM 启动应用
|
||||
%CMD%
|
||||
104
start.sh
Normal file
104
start.sh
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
# WPS Bot Game 启动脚本
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_WEBHOOK_URL=""
|
||||
DEFAULT_HOST="0.0.0.0"
|
||||
DEFAULT_PORT="11000"
|
||||
DEFAULT_WORKERS="1"
|
||||
DEFAULT_LOG_LEVEL="info"
|
||||
|
||||
# 显示帮助信息
|
||||
show_help() {
|
||||
echo "WPS Bot Game 启动脚本"
|
||||
echo ""
|
||||
echo "用法: $0 [选项]"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " -w, --webhook-url URL 设置WPS Webhook URL"
|
||||
echo " -H, --host HOST 服务器主机地址 (默认: $DEFAULT_HOST)"
|
||||
echo " -p, --port PORT 服务器端口 (默认: $DEFAULT_PORT)"
|
||||
echo " -W, --workers NUM 工作进程数 (默认: $DEFAULT_WORKERS)"
|
||||
echo " -l, --log-level LEVEL 日志级别 (默认: $DEFAULT_LOG_LEVEL)"
|
||||
echo " -h, --help 显示此帮助信息"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 -w 'https://xz.wps.cn/api/v1/webhook/send?key=your_key'"
|
||||
echo " $0 -w 'https://xz.wps.cn/api/v1/webhook/send?key=your_key' -p 8080"
|
||||
echo " $0 --webhook-url 'https://xz.wps.cn/api/v1/webhook/send?key=your_key' --port 8080 --log-level debug"
|
||||
}
|
||||
|
||||
# 解析命令行参数
|
||||
WEBHOOK_URL="$DEFAULT_WEBHOOK_URL"
|
||||
HOST="$DEFAULT_HOST"
|
||||
PORT="$DEFAULT_PORT"
|
||||
WORKERS="$DEFAULT_WORKERS"
|
||||
LOG_LEVEL="$DEFAULT_LOG_LEVEL"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-w|--webhook-url)
|
||||
WEBHOOK_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-H|--host)
|
||||
HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
-p|--port)
|
||||
PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-W|--workers)
|
||||
WORKERS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--log-level)
|
||||
LOG_LEVEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "未知参数: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 检查Python环境
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "错误: 未找到python3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查依赖
|
||||
if ! python3 -c "import fastapi, uvicorn" &> /dev/null; then
|
||||
echo "错误: 缺少必要的Python依赖"
|
||||
echo "请运行: pip install -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 构建启动命令
|
||||
CMD="python3 app.py --host $HOST --port $PORT --workers $WORKERS --log-level $LOG_LEVEL"
|
||||
|
||||
if [ -n "$WEBHOOK_URL" ]; then
|
||||
CMD="$CMD --webhook-url '$WEBHOOK_URL'"
|
||||
fi
|
||||
|
||||
# 显示启动信息
|
||||
echo "启动WPS Bot Game..."
|
||||
echo "主机: $HOST"
|
||||
echo "端口: $PORT"
|
||||
echo "工作进程: $WORKERS"
|
||||
echo "日志级别: $LOG_LEVEL"
|
||||
if [ -n "$WEBHOOK_URL" ]; then
|
||||
echo "Webhook URL: $WEBHOOK_URL"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 启动应用
|
||||
eval $CMD
|
||||
44
start_background.sh
Executable file
44
start_background.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LiuBai网站后台启动脚本
|
||||
|
||||
PID_FILE="./liubai_web.pid"
|
||||
LOG_FILE="./liubai_web.log"
|
||||
|
||||
# 检查是否已经在运行
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "服务已经在运行 (PID: $PID)"
|
||||
exit 1
|
||||
else
|
||||
echo "删除旧的PID文件..."
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "正在启动LiuBaiBlog网站服务器..."
|
||||
|
||||
# 使用nohup在后台运行
|
||||
nohup venv/bin/python3 -m jurigged -v app.py > "$LOG_FILE" 2>&1 &
|
||||
PID=$!
|
||||
|
||||
# 保存PID
|
||||
echo $PID > "$PID_FILE"
|
||||
|
||||
echo "服务器已在后台启动!"
|
||||
echo "PID: $PID"
|
||||
echo "日志文件: $LOG_FILE"
|
||||
echo "要停止服务,请运行: ./stop_background.sh"
|
||||
|
||||
# 等待一下确保服务器启动
|
||||
sleep 2
|
||||
|
||||
# 检查进程是否还在运行
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "服务器启动成功!"
|
||||
else
|
||||
echo "服务器启动失败,请检查日志文件: $LOG_FILE"
|
||||
rm -f "$PID_FILE"
|
||||
exit 1
|
||||
fi
|
||||
38
stop_background.sh
Executable file
38
stop_background.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LiuBai网站后台停止脚本
|
||||
|
||||
PID_FILE="./liubai_web.pid"
|
||||
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
echo "服务未运行或PID文件不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
|
||||
echo "正在停止服务器 (PID: $PID)..."
|
||||
|
||||
# 尝试优雅地停止进程
|
||||
if kill -TERM "$PID" 2>/dev/null; then
|
||||
echo "发送停止信号..."
|
||||
|
||||
# 等待进程结束
|
||||
for i in {1..10}; do
|
||||
if ! kill -0 "$PID" 2>/dev/null; then
|
||||
echo "服务器已停止"
|
||||
rm -f "$PID_FILE"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 如果进程还在运行,强制杀死
|
||||
echo "强制停止进程..."
|
||||
kill -KILL "$PID" 2>/dev/null
|
||||
rm -f "$PID_FILE"
|
||||
echo "服务器已强制停止"
|
||||
else
|
||||
echo "进程不存在或已停止"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
@@ -2,7 +2,7 @@
|
||||
import httpx
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from config import WEBHOOK_URL
|
||||
from config import GetWebhookURL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -10,12 +10,14 @@ logger = logging.getLogger(__name__)
|
||||
class MessageSender:
|
||||
"""消息发送器"""
|
||||
|
||||
def __init__(self, webhook_url: str = WEBHOOK_URL):
|
||||
def __init__(self, webhook_url: Optional[str] = None):
|
||||
"""初始化消息发送器
|
||||
|
||||
Args:
|
||||
webhook_url: Webhook URL
|
||||
"""
|
||||
if webhook_url is None:
|
||||
webhook_url = GetWebhookURL()
|
||||
self.webhook_url = webhook_url
|
||||
self.client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
@@ -128,5 +130,45 @@ def get_message_sender() -> MessageSender:
|
||||
global _sender_instance
|
||||
if _sender_instance is None:
|
||||
_sender_instance = MessageSender()
|
||||
else:
|
||||
# 更新Webhook URL以确保使用最新的值
|
||||
_sender_instance.webhook_url = GetWebhookURL()
|
||||
return _sender_instance
|
||||
|
||||
|
||||
async def send_private_message(user_id: int, content: str, msg_type: str = 'text') -> bool:
|
||||
"""发送私聊消息到用户个人webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 目标用户ID
|
||||
content: 消息内容
|
||||
msg_type: 消息类型 ('text' 或 'markdown')
|
||||
|
||||
Returns:
|
||||
是否发送成功,如果用户没有个人URL则返回False
|
||||
"""
|
||||
from core.database import get_db
|
||||
|
||||
# 从数据库获取用户webhook URL
|
||||
db = get_db()
|
||||
webhook_url = db.get_user_webhook_url(user_id)
|
||||
|
||||
if not webhook_url:
|
||||
logger.warning(f"用户 {user_id} 没有注册个人webhook URL,无法发送私聊消息")
|
||||
return False
|
||||
|
||||
# 创建MessageSender实例(使用用户的个人URL)
|
||||
sender = MessageSender(webhook_url=webhook_url)
|
||||
|
||||
try:
|
||||
# 根据msg_type调用相应方法
|
||||
if msg_type == 'markdown':
|
||||
return await sender.send_markdown(content)
|
||||
else:
|
||||
return await sender.send_text(content)
|
||||
except Exception as e:
|
||||
logger.error(f"发送私聊消息失败: user_id={user_id}, error={e}", exc_info=True)
|
||||
return False
|
||||
finally:
|
||||
# 关闭HTTP客户端
|
||||
await sender.close()
|
||||
|
||||
@@ -11,6 +11,10 @@ class CommandParser:
|
||||
|
||||
# 指令映射表
|
||||
COMMAND_MAP = {
|
||||
# 用户注册系统(必须在骰娘之前)
|
||||
'.register': 'register',
|
||||
'.注册': 'register',
|
||||
|
||||
# 骰娘
|
||||
'.r': 'dice',
|
||||
'.roll': 'dice',
|
||||
@@ -30,6 +34,49 @@ class CommandParser:
|
||||
'.quiz': 'quiz',
|
||||
'.问答': 'quiz',
|
||||
|
||||
# 成语接龙
|
||||
'.idiom': 'idiom',
|
||||
'.成语接龙': 'idiom',
|
||||
'.成语': 'idiom',
|
||||
|
||||
# 五子棋
|
||||
'.gomoku': 'gomoku',
|
||||
'.五子棋': 'gomoku',
|
||||
'.gobang': 'gomoku',
|
||||
|
||||
# 积分系统
|
||||
'.points': 'points',
|
||||
'.积分': 'points',
|
||||
'.checkin': 'points',
|
||||
'.签到': 'points',
|
||||
'.打卡': 'points',
|
||||
|
||||
# 炼金系统
|
||||
'.alchemy': 'alchemy',
|
||||
'.炼金': 'alchemy',
|
||||
|
||||
# 冒险系统
|
||||
'.adventure': 'adventure',
|
||||
'.冒险': 'adventure',
|
||||
|
||||
# 积分赠送系统
|
||||
'.gift': 'gift',
|
||||
'.赠送': 'gift',
|
||||
'.送': 'gift',
|
||||
|
||||
# AI对话系统
|
||||
'.ai': 'ai_chat',
|
||||
'.aiconfig': 'ai_chat',
|
||||
|
||||
# 复述
|
||||
'.say': 'say',
|
||||
'.说': 'say',
|
||||
'.复述': 'say',
|
||||
|
||||
# 私聊
|
||||
'.talk': 'talk',
|
||||
'.私聊': 'talk',
|
||||
|
||||
# 帮助
|
||||
'.help': 'help',
|
||||
'.帮助': 'help',
|
||||
@@ -37,10 +84,18 @@ class CommandParser:
|
||||
# 统计
|
||||
'.stats': 'stats',
|
||||
'.统计': 'stats',
|
||||
|
||||
# 赌场系统
|
||||
'.赌场': 'casino',
|
||||
'.casino': 'casino',
|
||||
|
||||
# 狼人杀系统
|
||||
'.werewolf': 'werewolf',
|
||||
'.狼人杀': 'werewolf',
|
||||
}
|
||||
|
||||
# 机器人名称模式(用于从@消息中提取)
|
||||
AT_PATTERN = re.compile(r'@\s*\S+\s+(.+)', re.DOTALL)
|
||||
AT_PATTERN = re.compile(r'@[^\s]+\s+(.+)', re.DOTALL)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, content: str) -> Optional[Tuple[str, str]]:
|
||||
@@ -60,11 +115,26 @@ class CommandParser:
|
||||
if at_match:
|
||||
content = at_match.group(1).strip()
|
||||
|
||||
# 检查是否以指令开头
|
||||
for cmd_prefix, game_type in cls.COMMAND_MAP.items():
|
||||
if content.startswith(cmd_prefix):
|
||||
# 返回游戏类型和完整指令
|
||||
return game_type, content
|
||||
# 拦截全角空格与全角标点(不允许)
|
||||
# 范围包含:全角空格\u3000、全角标点\uFF01-\uFF60、兼容区\uFFE0-\uFFEE
|
||||
# if re.search(r"[\u3000\uFF01-\uFF60\uFFE0-\uFFEE]", content):
|
||||
# logger.debug(f"包含全角字符,忽略: {content}")
|
||||
# return None
|
||||
|
||||
# 大小写不敏感匹配(仅用于匹配,不改变返回的原始内容)
|
||||
content_lower = content.lower()
|
||||
|
||||
# 使用最长前缀优先,避免 .r 误匹配 .roll 等更长前缀
|
||||
command_keys_sorted = sorted(cls.COMMAND_MAP.keys(), key=len, reverse=True)
|
||||
for cmd_prefix in command_keys_sorted:
|
||||
if content_lower.startswith(cmd_prefix.lower()):
|
||||
return cls.COMMAND_MAP[cmd_prefix], content
|
||||
|
||||
# 特殊处理:.ai 和 .aiconfig 指令支持参数
|
||||
if content.startswith('.ai '):
|
||||
return 'ai_chat', content
|
||||
if content.startswith('.aiconfig '):
|
||||
return 'ai_chat', content
|
||||
|
||||
# 没有匹配的指令
|
||||
logger.debug(f"未识别的指令: {content}")
|
||||
|
||||
53
verify_webhook.py
Normal file
53
verify_webhook.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""验证Webhook URL动态更新功能"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def verify_webhook_url():
|
||||
"""验证Webhook URL动态更新功能"""
|
||||
print("验证Webhook URL动态更新功能...")
|
||||
|
||||
# 1. 导入配置模块
|
||||
import config
|
||||
original_url = config.WEBHOOK_URL
|
||||
print(f"原始Webhook URL: {original_url}")
|
||||
|
||||
# 2. 设置新的环境变量
|
||||
new_url = "https://verified.test.com/webhook?key=verified_key"
|
||||
os.environ['WEBHOOK_URL'] = new_url
|
||||
print(f"设置新的环境变量: {new_url}")
|
||||
|
||||
# 3. 重新加载配置模块
|
||||
importlib.reload(config)
|
||||
updated_url = config.WEBHOOK_URL
|
||||
print(f"更新后的Webhook URL: {updated_url}")
|
||||
|
||||
# 4. 验证更新是否成功
|
||||
if updated_url == new_url:
|
||||
print("✅ Webhook URL动态更新功能正常!")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Webhook URL动态更新功能异常! 期望: {new_url}, 实际: {updated_url}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("WPS Bot Game Webhook URL动态更新验证")
|
||||
print("=" * 50)
|
||||
|
||||
success = verify_webhook_url()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
if success:
|
||||
print("🎉 验证通过! 现在可以使用 -w 参数指定Webhook URL了!")
|
||||
print("\n使用方法:")
|
||||
print("python app.py --webhook-url 'https://your-webhook-url'")
|
||||
print("python app.py -w 'https://your-webhook-url'")
|
||||
else:
|
||||
print("❌ 验证失败! 需要进一步修复!")
|
||||
print("=" * 50)
|
||||
Reference in New Issue
Block a user