54 Commits

Author SHA1 Message Date
3f3c21c3d6 修复停牌操作的字典key错误 2025-10-31 16:27:15 +08:00
b07b044035 21点游戏规则更新 2025-10-31 15:57:26 +08:00
9e16774d2f 修复21点中的结算错误 2025-10-31 15:39:53 +08:00
17da2e6111 修复数据库错误 2025-10-31 12:19:00 +08:00
1f10799562 实现21点与轮盘游戏 2025-10-31 12:09:07 +08:00
cef684f64b Save 2025-10-31 11:58:35 +08:00
7962852685 feat: 鎵╁睍鏁版嵁搴撴敮鎸佽疆鐩樺拰21鐐规父鎴?- 娣诲姞鍒楀瓨鍦ㄦ€ф鏌ヨ緟鍔╂柟娉?- 鎵╁睍casino_sessions鍜宑asino_bets琛ㄦ坊鍔犳柊瀛楁 - 鍒涘缓casino_blackjack_hands琛ㄥ瓨鍌?1鐐规墜鐗?- 淇敼create_casino_session鏀寔鍗曞満闄愬埗鍜屾柊瀛楁 - 鎵╁睍create_casino_bet鏀寔杞洏鍜?1鐐逛笓鐢ㄥ瓧娈?- 娣诲姞21鐐规墜鐗岀鐞嗘柟娉曢泦 2025-10-31 11:46:29 +08:00
cc97374e98 同步get_user_display_name 2025-10-31 11:16:38 +08:00
a62d5d66b7 t branch
Merge branch 'task/add-idiom-chain-game_2025-10-28_1'
2025-10-31 11:10:55 +08:00
19cde88acf 更新统一接口BaseGame.db.get_user_display_name获取用户名称 2025-10-31 11:07:41 +08:00
6f05ca98f1 修复冒险完成后不回收奖励就进行炼金会丢失奖励的问题 2025-10-31 10:30:29 +08:00
5a8a4ac026 比大小游戏修改 2025-10-30 17:27:15 +08:00
12aac846cc 新增赌场系统 2025-10-30 16:11:50 +08:00
4bddd4339f 发现的加分覆盖签到与长时间任务被清理的错误, 已经在数据库修复中尝试解决 2025-10-30 15:03:00 +08:00
927c16e1fc 新增say指令 2025-10-30 14:27:38 +08:00
8ffd261bb0 1.修改ai返回格式为markdown2.新增冒险放弃指令 2025-10-30 11:57:46 +08:00
3d4f754a0a Merge branch 'main' of http://www.liubai.site:3000/ninemine/WPSBot 2025-10-30 11:05:27 +08:00
0a60a7fc4b 修复指令解析错误 2025-10-30 11:05:08 +08:00
82dc616495 Merge feature: AI瀵硅瘽鍔熻兘瀹炵幇瀹屾垚 2025-10-30 01:11:29 +08:00
e26936acee feat: 瀹屾垚AI瀵硅瘽鍔熻兘骞跺疄鐜扮郴缁熸彁绀鸿瘝鎸佷箙鍖?
- 瀹炵幇鍩轰簬llama_index + Ollama鐨凙I瀵硅瘽鍔熻兘
- 娣诲姞.ai鍜?aiconfig鎸囦护鏀寔
- 瀹炵幇鍥哄畾10绉掔瓑寰呯獥鍙g殑寤惰繜鍥炵瓟鏈哄埗
- 鏀寔澶氱敤鎴峰璇濆拰鐢ㄦ埛瑙掕壊鏄犲皠
- 瀹炵幇闀夸笂涓嬫枃绠$悊锛?0+杞璇濓級
- 绯荤粺鎻愮ず璇嶆寔涔呭寲鍒伴厤缃枃浠?- 浼樺寲閿欒澶勭悊鍜岃皟璇曚俊鎭?- 娣诲姞NPS绔彛杞彂鏀寔璇存槑
2025-10-30 01:10:59 +08:00
643c516baf 提示词增强 2025-10-30 01:05:03 +08:00
e2e039b9b1 修复一些参数错误并提升一些提示内容 2025-10-30 00:53:37 +08:00
a76de359d5 修改配置并取消每次接受消息后的自动回复 2025-10-30 00:35:52 +08:00
b247c57bbe 新增AI会话系统 2025-10-29 23:56:57 +08:00
a893e54166 1.修改命令的判断条件(从startwith转为判定相等)2.修改冒险系统开发任务文件 2025-10-29 17:52:15 +08:00
8eb9f26cfd 新增冒险系统(成功, 编写任务文件) 2025-10-29 17:38:20 +08:00
01248b9092 新增冒险系统 2025-10-29 17:28:02 +08:00
9504c57aaf 新增冒险系统 2025-10-29 17:26:57 +08:00
5742adc2ad 新增冒险系统 2025-10-29 17:23:24 +08:00
9718eb0614 1.存在匹配上的问题, 后期再进行修复, 目前治标 2025-10-29 16:03:08 +08:00
27aee22f62 新增注册系统 2025-10-29 15:49:39 +08:00
37fccb3021 1.数值转整形2.炼金期望调整为1.1左右 2025-10-29 15:30:45 +08:00
218e1f1045 炼金期望调整 2025-10-29 15:15:34 +08:00
b57f1e08ef 修复了积分系统中指令分析的错误 2025-10-29 15:07:49 +08:00
581a516610 尝试修复积分系统 2025-10-29 12:56:57 +08:00
8e883fe5e1 恢复误删的内容 2025-10-29 12:41:56 +08:00
5e2afca960 尝试修复积分系统 2025-10-29 12:36:20 +08:00
0c2638e948 1.尝试修复积分系统2.大幅度简化积分系统 2025-10-29 12:29:18 +08:00
7b72ed9f34 Save 2025-10-29 12:19:25 +08:00
08cc4870ca 修复webhook url指定 2025-10-29 12:16:12 +08:00
c80fb67165 修复webhook url指定 2025-10-29 12:14:50 +08:00
d36bb2de83 修复webhook url指定无效的问题 2025-10-29 12:11:28 +08:00
38e81dbfe6 新增webhook url参数 2025-10-29 12:06:18 +08:00
003ff9a94e 1.尝试修复签到不加分的错误2.移除注册时间显示 2025-10-29 11:51:15 +08:00
7483a00a99 积分系统 2025-10-29 11:32:43 +08:00
c05a8b3578 更新README 2025-10-29 11:04:04 +08:00
1439523253 Merge pull request '合并task/gomoku_2025-10-28_1五子棋游戏' (#1) from task/gomoku_2025-10-28_1 into main
Reviewed-on: #1
2025-10-29 09:32:13 +08:00
ninemine
278e760fb2 棋盘对齐 2025-10-29 09:29:42 +08:00
57f955f837 修改棋盘格式用于对齐 2025-10-28 17:57:02 +08:00
7d28e2e2aa 修改为发起/接受对局以避开at无法获取id的问题 2025-10-28 17:49:16 +08:00
f217cd958b 修复未成功, 添加调试 2025-10-28 17:43:21 +08:00
93a4882da2 修复at用户格式错误 2025-10-28 17:37:06 +08:00
38cd441908 新增五子棋游戏 2025-10-28 17:22:49 +08:00
b7a57539f5 Merge branch 'task/add-idiom-chain-game_2025-10-28_1' 2025-10-28 16:46:05 +08:00
36 changed files with 8163 additions and 330 deletions

3
.gitignore vendored
View File

@@ -181,3 +181,6 @@ cython_debug/
.cursorindexingignore
# IDE
.vscode/
# Database
data/bot.db

View 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)`作为唯一键
- 五子棋需要双人对战,需要同时记录两个玩家
- 需要设计状态数据结构,存储:
- 对战双方IDplayer1_id, player2_id
- 当前轮到谁current_player_id
- 棋盘状态15x15二维数组
- 游戏状态waiting, playing, finished
- 胜者IDwinner_id如果有
### 2. 多轮对战并发
- 允许同一个chat中有多轮对战只要对战双方不同
- 需要一个机制来标识不同的对战局可以用对战双方ID的组合
- 状态查询需要能够找到特定用户参与的对战
### 3. 禁手规则实现
禁手规则(仅对黑方,即先手玩家):
- **三三禁手**:一手棋同时形成两个或以上的活三
- **四四禁手**:一手棋同时形成两个或以上的活四或冲四
- **长连禁手**:一手棋形成六子或以上的连珠
需要实现:
- 判断某个位置的四个方向(横、竖、左斜、右斜)的连珠情况
- 判断活三、活四、冲四的定义
- 在落子时检查是否触发禁手
### 4. 坐标系统
-A-O15列
-1-1515行
- 需要坐标转换函数:`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代码块中正确对齐
- 原因:修复用户反馈的棋盘文本对齐问题
- 阻碍因素:无
- 状态:未确认
# 最终审查
(待完成后填写)

View 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 的问题,以及@前缀处理不完整的问题
- 阻碍因素:无
- 状态:未确认
# 最终审查
[等待实施]

View 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系统中。

View 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/ # 数据文件
```
## 技术栈
- FastAPIWeb框架
- SQLite数据存储
- llama-index-coreAI对话框架核心
- llama-index-llms-ollamaOllama 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 文本被当作纯文本发送导致的排版问题。
- 阻碍因素:
- 暂无。
- 状态:
- 成功。

View File

@@ -0,0 +1,506 @@
# 背景
文件名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纯数据库表方案推荐
**优点**
- 数据结构清晰,便于查询统计
- 支持历史记录追踪
- 并发安全,利用数据库事务
- 易于扩展复杂查询
**缺点**
- 需要维护额外的表结构
- 稍微复杂一些
**决策**:采用此方案
### 方案Bgame_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个平局玩家未被结算
- 阻碍因素:无
- 状态:成功
# 最终审查
待完成

175
README.md
View File

@@ -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
View 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
```

43
app.py
View File

@@ -1,11 +1,13 @@
"""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
@@ -94,14 +96,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
View 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

View File

@@ -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"))
@@ -68,5 +77,8 @@ GAME_CONFIG = {
"平步青云", "云程发轫", "刃迎缕解", "解甲归田"
]
},
}
"gomoku": {
"max_concurrent_games": 5, # 每个聊天最多同时进行的游戏数
"board_size": 15, # 棋盘大小
},
}

File diff suppressed because it is too large Load Diff

6
data/ai_config.json Normal file
View File

@@ -0,0 +1,6 @@
{
"host": "0.0.0.0",
"port": 11434,
"model": "qwen3:0.6b"
}

Binary file not shown.

View File

@@ -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": "庆祝你的成就,准备迎接新的循环。"
}
]
}

View File

@@ -1,5 +1,6 @@
{
"blacklist": [],
"description": "成语接龙游戏全局黑名单,被拒绝的词语将永久不可使用"
"blacklist": [],
"description": "成语接龙游戏全局黑名单,被拒绝的词语将永久不可使用"
}

View File

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

287
games/adventure.py Normal file
View File

@@ -0,0 +1,287 @@
"""冒险系统游戏模块"""
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 AdventureGame(BaseGame):
"""冒险系统游戏"""
def __init__(self):
"""初始化游戏"""
super().__init__()
self.db = get_db()
# 奖品池配置
self.prize_pool: List[Tuple[int, float, str]] = [
# (权重, 倍率, 描述)
(500, 0.5, "少量积分"),
(350, 1, "中等积分"),
(200, 2, "大量积分"),
(100, 5, "丰厚积分"),
(50, 10, "丰厚积分"),
(10, 100, "🌟 巨额积分"),
(1, 1000, "💎 传说积分"),
]
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: 指令,如 ".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.isdigit():
cost_time = int(args)
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 * 60
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)
# 格式化输出
text = f"## ⚡️ 冒险结果\n\n"
text += f"**消耗时间**: {saved_cost_time} 分钟\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:冒险未完成,返回等待提示
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}"
text = f"## ⚡️ 冒险进行中\n\n"
text += f"你正在进行一次冒险,还需等待 **{wait_msg}** 才能完成。\n\n"
text += f"**当前冒险时长**: {saved_cost_time} 分钟\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 * 60
end_datetime = datetime.fromtimestamp(end_time)
end_time_str = end_datetime.strftime('%H:%M:%S')
text = f"## ⚡️ 冒险开始\n\n"
text += f"你已经开始了冒险之旅,本次冒险将持续 **{cost_time}** 分钟。\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))
elapsed_minutes = elapsed_seconds // 60
if elapsed_minutes < 1:
elapsed_minutes = 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_minutes)
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)
# 输出
text = f"## ⚡️ 冒险放弃\n\n"
text += f"**已计入时间**: {elapsed_minutes} 分钟\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 time` - 消耗time分钟进行冒险, 最少一分钟\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
View 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
View 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, "炼金失败"),
(100, "penalty", -1, "炼金爆炸"),
(100, "points", 0.1, "少量积分"),
(390, "points", 0.5, "少量积分"),
(500, "points", 1, "等值积分"),
(390, "points", 2, "丰厚积分"),
(200, "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()

View File

@@ -73,6 +73,71 @@ def get_help_message() -> str:
- `.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` - 庄家放弃游戏(返还下注)
### 其他
- `.help` - 显示帮助
- `.stats` - 查看个人统计
@@ -105,7 +170,8 @@ def get_stats_message(user_id: int) -> str:
'rps': '✊ 石头剪刀布',
'guess': '🔢 猜数字',
'quiz': '📝 问答游戏',
'idiom': '🀄 成语接龙'
'idiom': '🀄 成语接龙',
'gomoku': '⚫ 五子棋'
}
for row in stats:

1473
games/casino.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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

266
games/gift.py Normal file
View File

@@ -0,0 +1,266 @@
"""积分赠送系统游戏模块"""
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 points > 1000:
return "❌ 单次赠送积分不能超过1000"
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
View 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-O15列
- 行1-1515行
- 示例A1左上角、O15右下角、H8中心
### 示例
```
.gomoku challenge # 发起挑战
.gomoku accept # 接受挑战
.gomoku H8 # 在中心位置落子
.gomoku show # 查看棋盘
.gomoku resign # 认输
```
💡 提示:黑方虽然先手,但需要注意禁手规则
"""

287
games/gomoku_logic.py Normal file
View 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)

View File

@@ -379,10 +379,12 @@ class IdiomGame(BaseGame):
text += f"**当前链长**{state_data['chain_length']}\n\n"
user_count = state_data['participants'][str(user_id)]
text += f"@用户{user_id} 成功次数:{user_count}\n\n"
user_display_name = self.db.get_user_display_name(user_id)
text += f"@{user_display_name} 成功次数:{user_count}\n\n"
if mentioned_user_id:
text += f"已指定 @用户{mentioned_user_id} 接龙\n\n"
mentioned_display_name = self.db.get_user_display_name(mentioned_user_id)
text += f"已指定 @{mentioned_display_name} 接龙\n\n"
else:
text += "任何人都可以接龙\n\n"
@@ -416,7 +418,8 @@ class IdiomGame(BaseGame):
state_data['next_user_id'] = next_user_id
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
return f"✅ 已指定 @用户{next_user_id} 接龙"
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:
"""裁判拒绝词语
@@ -509,7 +512,8 @@ class IdiomGame(BaseGame):
# 下一位
if state_data.get('next_user_id'):
text += f"**下一位**@用户{state_data['next_user_id']}\n\n"
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"
@@ -522,7 +526,8 @@ class IdiomGame(BaseGame):
reverse=True
)
for idx, (uid, count) in enumerate(sorted_participants[:5], 1):
text += f"{idx}. @用户{uid} - {count}\n"
user_display_name = self.db.get_user_display_name(int(uid))
text += f"{idx}. @{user_display_name} - {count}\n"
text += "\n"
# 最近成语
@@ -588,7 +593,8 @@ class IdiomGame(BaseGame):
reverse=True
)
for idx, (uid, count) in enumerate(sorted_participants, 1):
text += f"{idx}. @用户{uid} - {count}\n"
user_display_name = self.db.get_user_display_name(int(uid))
text += f"{idx}. @{user_display_name} - {count}\n"
# 更新统计
try:
for _ in range(count):

184
games/points.py Normal file
View 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
```
### 说明
- 每日签到只能进行一次
- 运势积分每次查看都有机会获得
- 积分系统已简化,不再保留历史记录
"""

View File

@@ -18,5 +18,9 @@ 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

View File

@@ -28,7 +28,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:
@@ -75,14 +76,20 @@ async def callback_receive(request: Request):
# 发送回复
if response_text:
sender = get_message_sender()
# 根据内容选择消息类型
if response_text.startswith('#'):
# Markdown格式
await sender.send_markdown(response_text)
# 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:
# 普通文本
await sender.send_text(response_text)
# 其他模块保持原有启发式:以 # 开头视为 Markdown否则文本
if response_text.startswith('#'):
await sender.send_markdown(response_text)
else:
await sender.send_text(response_text)
return JSONResponse({"result": "ok"})
@@ -116,6 +123,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
@@ -152,6 +163,57 @@ async def handle_command(game_type: str, command: str,
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
# 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)
# 未知游戏类型
logger.warning(f"未知游戏类型: {game_type}")
return "❌ 未知的游戏类型"
@@ -160,3 +222,40 @@ 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"
chat_id: 会话ID
user_id: 用户ID
Returns:
注册结果消息
"""
try:
# 提取参数
_, args = CommandParser.extract_command_args(command)
args = args.strip()
# 验证参数
if not args:
return "❌ 请提供要注册的名称!\n\n正确格式:`.register <名称>`\n\n示例:\n`.register 张三`\n`.register 小明`"
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)}"

150
start.bat Normal file
View 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
View 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

View File

@@ -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,8 @@ 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

View File

@@ -11,6 +11,10 @@ class CommandParser:
# 指令映射表
COMMAND_MAP = {
# 用户注册系统(必须在骰娘之前)
'.register': 'register',
'.注册': 'register',
# 骰娘
'.r': 'dice',
'.roll': 'dice',
@@ -35,6 +39,40 @@ class CommandParser:
'.成语接龙': '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',
# 帮助
'.help': 'help',
'.帮助': 'help',
@@ -42,10 +80,14 @@ class CommandParser:
# 统计
'.stats': 'stats',
'.统计': 'stats',
# 赌场系统
'.赌场': 'casino',
'.casino': 'casino',
}
# 机器人名称模式(用于从@消息中提取)
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]]:
@@ -65,11 +107,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
View 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)