新增插件指引网页
This commit is contained in:
@@ -10,7 +10,7 @@ alwaysApply: true
|
||||
|
||||
语言设置:除非用户另有指示,所有常规交互响应都应该使用中文。然而,模式声明(例如\[MODE: RESEARCH\])和特定格式化输出(例如代码块、清单等)应保持英文,以确保格式一致性。
|
||||
|
||||
工具调用: 你应该使用工具调用而不是通过命令行编辑文件
|
||||
**工具调用: 你应该使用工具调用而不是通过命令行编辑文件**
|
||||
|
||||
你必须完全理解这个项目, 并明白文件夹PWF里的文件你没有权力更改
|
||||
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-10-28_1_add-idiom-chain-game.md
|
||||
创建于:2025-10-28_15:43:00
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/add-idiom-chain-game_2025-10-28_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在WPS Bot Game项目中新增一个成语接龙游戏功能。
|
||||
|
||||
## 核心需求
|
||||
1. 群内多人游戏,机器人作为裁判和出题者
|
||||
2. 允许按拼音接龙(包括谐音接龙)
|
||||
3. 没有时间限制
|
||||
4. 不需要提示功能
|
||||
5. 游戏记录保存到.stats统计中
|
||||
6. 不允许重复使用成语
|
||||
7. 不需要难度分级(非人机对战)
|
||||
8. 需要裁判指令用于接受/拒绝玩家回答
|
||||
|
||||
## 游戏玩法
|
||||
- 机器人出题(给出起始成语)
|
||||
- 群内玩家轮流接龙
|
||||
- 机器人判断接龙是否有效(拼音/谐音匹配、未重复使用)
|
||||
- 裁判可以手动接受或拒绝某个回答
|
||||
- 记录每个玩家的成功接龙次数
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/
|
||||
│ ├── database.py # SQLite数据库操作
|
||||
│ ├── middleware.py # 中间件
|
||||
│ └── models.py # 数据模型
|
||||
├── games/ # 游戏模块
|
||||
│ ├── base.py # 游戏基类
|
||||
│ ├── dice.py # 骰娘游戏
|
||||
│ ├── rps.py # 石头剪刀布
|
||||
│ ├── fortune.py # 运势占卜
|
||||
│ ├── guess.py # 猜数字
|
||||
│ └── quiz.py # 问答游戏
|
||||
├── data/ # 数据文件
|
||||
│ ├── bot.db # SQLite数据库
|
||||
│ ├── quiz.json # 问答题库
|
||||
│ └── fortunes.json # 运势数据
|
||||
├── routers/ # 路由处理
|
||||
│ ├── callback.py # WPS回调处理
|
||||
│ └── health.py # 健康检查
|
||||
└── utils/ # 工具模块
|
||||
├── message.py # 消息发送
|
||||
├── parser.py # 指令解析
|
||||
└── rate_limit.py # 限流控制
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
- FastAPI:Web框架
|
||||
- SQLite:数据存储
|
||||
- WPS协作机器人API:消息接收与发送
|
||||
|
||||
## 现有游戏架构
|
||||
1. 所有游戏继承`BaseGame`基类
|
||||
2. 必须实现`handle(command, chat_id, user_id)`方法处理指令
|
||||
3. 必须实现`get_help()`方法返回帮助信息
|
||||
4. 游戏状态存储在数据库`game_states`表:`(chat_id, user_id, game_type)`作为联合主键
|
||||
5. 游戏统计存储在`game_stats`表:记录`wins`, `losses`, `draws`, `total_plays`
|
||||
6. 指令通过`CommandParser`解析,在`callback.py`中分发到对应游戏处理器
|
||||
|
||||
## 数据库设计
|
||||
### game_states表
|
||||
- chat_id: 会话ID
|
||||
- user_id: 用户ID
|
||||
- game_type: 游戏类型
|
||||
- state_data: JSON格式的游戏状态数据
|
||||
- created_at/updated_at: 时间戳
|
||||
|
||||
### game_stats表
|
||||
- user_id: 用户ID
|
||||
- game_type: 游戏类型
|
||||
- wins/losses/draws/total_plays: 统计数据
|
||||
|
||||
# 分析
|
||||
|
||||
## 关键技术挑战
|
||||
|
||||
### 1. 群级别vs个人级别状态管理
|
||||
现有游戏(猜数字、问答)都是个人独立状态,使用`(chat_id, user_id, game_type)`作为主键。
|
||||
|
||||
成语接龙是群内共享游戏,需要:
|
||||
- 群级别的游戏状态:当前成语、已用成语列表、接龙长度、当前轮到谁
|
||||
- 个人级别的统计:每个玩家的成功接龙次数
|
||||
|
||||
**可能方案:**
|
||||
- 使用特殊user_id(如0或-1)存储群级别游戏状态
|
||||
- 或者在state_data中存储所有玩家信息
|
||||
|
||||
### 2. 成语词库准备
|
||||
需要准备:
|
||||
- 成语列表(至少500-1000个常用成语)
|
||||
- 每个成语的拼音信息(用于判断接龙是否匹配)
|
||||
- 数据格式:JSON文件,类似quiz.json
|
||||
|
||||
### 3. 拼音匹配逻辑
|
||||
- 需要拼音库支持(pypinyin)
|
||||
- 支持谐音匹配(声母韵母匹配)
|
||||
- 处理多音字情况
|
||||
|
||||
### 4. 裁判指令设计
|
||||
需要额外指令:
|
||||
- `.idiom accept` - 接受上一个回答
|
||||
- `.idiom reject` - 拒绝上一个回答
|
||||
- 需要权限控制(谁可以当裁判?)
|
||||
|
||||
### 5. 游戏流程设计
|
||||
```
|
||||
1. 开始游戏:.idiom start
|
||||
- 机器人随机给出起始成语
|
||||
- 创建群级别游戏状态
|
||||
|
||||
2. 玩家接龙:.idiom [成语]
|
||||
- 检查是否在词库中
|
||||
- 检查拼音是否匹配(首字拼音 == 上一个成语尾字拼音)
|
||||
- 检查是否已使用过
|
||||
- 自动判断或等待裁判确认
|
||||
|
||||
3. 裁判操作:.idiom accept/reject
|
||||
- 手动接受或拒绝最近的回答
|
||||
|
||||
4. 查看状态:.idiom status
|
||||
- 显示当前成语、已用成语数量、参与人数
|
||||
|
||||
5. 结束游戏:.idiom stop
|
||||
- 显示统计信息
|
||||
- 更新每个玩家的game_stats
|
||||
```
|
||||
|
||||
## 现有代码分析
|
||||
|
||||
### CommandParser (utils/parser.py)
|
||||
需要添加成语接龙指令映射:
|
||||
```python
|
||||
'.idiom': 'idiom',
|
||||
'.成语接龙': 'idiom',
|
||||
```
|
||||
|
||||
### callback.py (routers/callback.py)
|
||||
需要在`handle_command`函数中添加idiom游戏分支:
|
||||
```python
|
||||
if game_type == 'idiom':
|
||||
from games.idiom import IdiomGame
|
||||
game = IdiomGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
### base.py (games/base.py)
|
||||
需要更新`get_help_message()`和`get_stats_message()`,添加成语接龙信息。
|
||||
|
||||
### config.py
|
||||
可能需要添加成语接龙相关配置:
|
||||
```python
|
||||
"idiom": {
|
||||
"auto_judge": True, # 是否自动判断
|
||||
"require_approval": False, # 是否需要裁判确认
|
||||
}
|
||||
```
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
(待INNOVATE模式填写)
|
||||
|
||||
# 当前执行步骤:"1. 创建任务文件"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## [2025-10-28 15:45:00]
|
||||
- 已修改:requirements.txt, config.py, utils/parser.py, routers/callback.py, games/base.py
|
||||
- 已创建:games/idiom.py
|
||||
- 更改:
|
||||
1. 在requirements.txt中添加pypinyin==0.51.0依赖
|
||||
2. 在config.py的GAME_CONFIG中添加idiom配置(起始成语池、历史显示数量)
|
||||
3. 在utils/parser.py的COMMAND_MAP中添加.idiom、.成语接龙、.成语指令映射
|
||||
4. 创建games/idiom.py实现完整的成语接龙游戏逻辑:
|
||||
- IdiomGame类继承BaseGame
|
||||
- 实现handle()主指令分发方法
|
||||
- 实现_start_game()开始游戏
|
||||
- 实现_make_chain()玩家接龙
|
||||
- 实现_set_next_user()指定下一位
|
||||
- 实现_reject_idiom()裁判拒绝
|
||||
- 实现_show_status()显示状态
|
||||
- 实现_show_blacklist()显示黑名单
|
||||
- 实现_stop_game()结束游戏
|
||||
- 实现_get_pinyin()获取拼音(支持多音字)
|
||||
- 实现_check_pinyin_match()检查拼音匹配(忽略声调)
|
||||
- 实现_parse_mentioned_user()解析@用户
|
||||
- 实现_can_answer()权限检查(防连续、指定轮次)
|
||||
- 实现_validate_idiom()词语验证(4字、拼音匹配、未使用、未黑名单)
|
||||
- 实现get_help()帮助信息
|
||||
5. 在routers/callback.py的handle_command()中添加idiom游戏分支
|
||||
6. 在games/base.py的get_help_message()中添加成语接龙帮助信息
|
||||
7. 在games/base.py的get_stats_message()的game_names字典中添加idiom映射
|
||||
- 原因:实现成语接龙游戏功能的所有核心代码
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
## [2025-10-28 15:50:00]
|
||||
- 已修改:games/base.py
|
||||
- 更改:在get_help_message()的成语接龙部分添加黑名单相关指令说明
|
||||
- 添加 `.idiom reject [词语]` - 拒绝词语加入黑名单(仅发起人)
|
||||
- 添加 `.idiom blacklist` - 查看黑名单
|
||||
- 原因:用户反馈.help帮助信息中看不到黑名单机制的使用说明
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
## [2025-10-28 15:55:00]
|
||||
- 已修改:games/idiom.py
|
||||
- 已创建:data/idiom_blacklist.json
|
||||
- 更改:将黑名单机制从游戏状态改为全局永久存储
|
||||
1. 创建data/idiom_blacklist.json作为全局黑名单数据文件
|
||||
2. 在IdiomGame.__init__()中添加黑名单文件路径和懒加载变量
|
||||
3. 添加_load_blacklist()方法从文件懒加载全局黑名单
|
||||
4. 添加_save_blacklist()方法保存黑名单到文件
|
||||
5. 修改_validate_idiom()方法检查全局黑名单而非游戏状态中的黑名单
|
||||
6. 修改_start_game()方法移除state_data中的blacklist字段初始化
|
||||
7. 修改_reject_idiom()方法将词语添加到全局黑名单并保存到文件
|
||||
8. 修改_show_blacklist()方法显示全局黑名单,不再依赖游戏状态
|
||||
9. 更新所有提示信息,明确说明是"永久禁用"
|
||||
- 原因:用户要求被拒绝的词语应该永久不可用,而不是仅本局游戏不可用
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
# 最终审查
|
||||
|
||||
(待REVIEW模式完成后填写)
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-10-28_1_gomoku.md
|
||||
创建于:2025-10-28_17:08:29
|
||||
创建者:User
|
||||
主分支:main
|
||||
任务分支:task/gomoku_2025-10-28_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
创建一个五子棋(Gomoku)游戏模块,支持双人对战功能。
|
||||
|
||||
## 核心需求
|
||||
1. **游戏模式**:双人对战(两个用户在同一个聊天中对战)
|
||||
2. **棋盘规格**:标准15x15棋盘
|
||||
3. **禁手规则**:需要实现禁手规则(三三禁手、四四禁手、长连禁手)
|
||||
4. **超时规则**:不需要回合时间限制
|
||||
5. **并发对战**:允许多轮对战同时存在,只要交战双方不同即可
|
||||
6. **显示方式**:使用emoji绘制棋盘(⚫⚪➕)+ 坐标系统(A-O列,1-15行)
|
||||
|
||||
## 功能清单
|
||||
- 开始游戏:`.gomoku start @对手` 或 `.gomoku @对手`
|
||||
- 落子:`.gomoku A1` 或 `.gomoku 落子 A1`
|
||||
- 认输:`.gomoku resign` 或 `.gomoku 认输`
|
||||
- 查看棋盘:`.gomoku show` 或 `.gomoku 查看`
|
||||
- 查看战绩:`.gomoku stats` 或 `.gomoku 战绩`
|
||||
- 帮助信息:`.gomoku help` 或 `.gomoku 帮助`
|
||||
|
||||
## 技术要点
|
||||
1. 继承`BaseGame`基类
|
||||
2. 游戏状态存储在数据库中(使用chat_id + 对战双方ID作为键)
|
||||
3. 需要实现五子棋禁手规则的判定逻辑
|
||||
4. 需要实现胜负判定(五子连珠)
|
||||
5. 棋盘使用二维数组表示,支持坐标转换(A-O, 1-15)
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 现有架构
|
||||
- **框架**:FastAPI
|
||||
- **数据库**:SQLite(使用标准库sqlite3)
|
||||
- **游戏基类**:`games/base.py - BaseGame`
|
||||
- **路由处理**:`routers/callback.py`
|
||||
- **数据库操作**:`core/database.py - Database类`
|
||||
|
||||
## 现有游戏
|
||||
- 石头剪刀布(rps)
|
||||
- 问答游戏(quiz)
|
||||
- 猜数字(guess)
|
||||
- 成语接龙(idiom)
|
||||
- 骰娘系统(dice)
|
||||
- 运势占卜(fortune)
|
||||
|
||||
## 数据库表结构
|
||||
1. **users**:用户基本信息
|
||||
2. **game_states**:游戏状态(支持chat_id, user_id, game_type的唯一约束)
|
||||
3. **game_stats**:游戏统计(wins, losses, draws, total_plays)
|
||||
|
||||
# 分析
|
||||
|
||||
## 核心挑战
|
||||
|
||||
### 1. 游戏状态管理
|
||||
- 现有的`game_states`表使用`(chat_id, user_id, game_type)`作为唯一键
|
||||
- 五子棋需要双人对战,需要同时记录两个玩家
|
||||
- 需要设计状态数据结构,存储:
|
||||
- 对战双方ID(player1_id, player2_id)
|
||||
- 当前轮到谁(current_player_id)
|
||||
- 棋盘状态(15x15二维数组)
|
||||
- 游戏状态(waiting, playing, finished)
|
||||
- 胜者ID(winner_id,如果有)
|
||||
|
||||
### 2. 多轮对战并发
|
||||
- 允许同一个chat中有多轮对战,只要对战双方不同
|
||||
- 需要一个机制来标识不同的对战局(可以用对战双方ID的组合)
|
||||
- 状态查询需要能够找到特定用户参与的对战
|
||||
|
||||
### 3. 禁手规则实现
|
||||
禁手规则(仅对黑方,即先手玩家):
|
||||
- **三三禁手**:一手棋同时形成两个或以上的活三
|
||||
- **四四禁手**:一手棋同时形成两个或以上的活四或冲四
|
||||
- **长连禁手**:一手棋形成六子或以上的连珠
|
||||
|
||||
需要实现:
|
||||
- 判断某个位置的四个方向(横、竖、左斜、右斜)的连珠情况
|
||||
- 判断活三、活四、冲四的定义
|
||||
- 在落子时检查是否触发禁手
|
||||
|
||||
### 4. 坐标系统
|
||||
- 列:A-O(15列)
|
||||
- 行:1-15(15行)
|
||||
- 需要坐标转换函数:`parse_coord("A1") -> (0, 0)`
|
||||
- 需要显示转换函数:`format_coord(0, 0) -> "A1"`
|
||||
|
||||
### 5. 棋盘显示
|
||||
使用emoji:
|
||||
- ⚫ 黑子(先手)
|
||||
- ⚪ 白子(后手)
|
||||
- ➕ 空位
|
||||
- 需要添加行号和列号标注
|
||||
|
||||
示例:
|
||||
```
|
||||
A B C D E F G H I J K L M N O
|
||||
1 ➕➕➕➕➕➕➕➕➕➕➕➕➕➕➕
|
||||
2 ➕➕➕➕➕➕➕➕➕➕➕➕➕➕➕
|
||||
3 ➕➕⚫➕➕➕➕➕➕➕➕➕➕➕➕
|
||||
...
|
||||
```
|
||||
|
||||
## 数据结构设计
|
||||
|
||||
### state_data结构
|
||||
```python
|
||||
{
|
||||
"player1_id": 123456, # 黑方(先手)
|
||||
"player2_id": 789012, # 白方(后手)
|
||||
"current_player": 123456, # 当前轮到谁
|
||||
"board": [[0]*15 for _ in range(15)], # 0:空, 1:黑, 2:白
|
||||
"status": "playing", # waiting, playing, finished
|
||||
"winner_id": None, # 胜者ID
|
||||
"moves": [], # 历史落子记录 [(row, col, player_id), ...]
|
||||
"last_move": None # 最后一手 (row, col)
|
||||
}
|
||||
```
|
||||
|
||||
### 游戏状态存储策略
|
||||
- 使用chat_id作为会话ID
|
||||
- 使用较小的user_id作为主键中的user_id(保证唯一性)
|
||||
- 在state_data中存储完整的对战信息
|
||||
- 查询时需要检查用户是否是player1或player2
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案选择
|
||||
使用现有的数据库表结构,通过精心设计state_data来支持双人对战。
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. 游戏类:`games/gomoku.py`
|
||||
继承`BaseGame`,实现以下方法:
|
||||
- `handle()` - 主处理逻辑
|
||||
- `get_help()` - 帮助信息
|
||||
- `_start_game()` - 开始游戏
|
||||
- `_make_move()` - 落子
|
||||
- `_show_board()` - 显示棋盘
|
||||
- `_resign()` - 认输
|
||||
- `_get_stats()` - 查看战绩
|
||||
|
||||
### 2. 五子棋逻辑:单独模块或工具类
|
||||
- `_parse_coord()` - 解析坐标
|
||||
- `_format_coord()` - 格式化坐标
|
||||
- `_render_board()` - 渲染棋盘
|
||||
- `_check_win()` - 检查胜负
|
||||
- `_check_forbidden()` - 检查禁手
|
||||
- `_is_valid_move()` - 检查落子是否合法
|
||||
|
||||
### 3. 禁手检测逻辑
|
||||
实现辅助方法:
|
||||
- `_count_line()` - 统计某方向的连珠情况
|
||||
- `_is_live_three()` - 判断活三
|
||||
- `_is_live_four()` - 判断活四
|
||||
- `_is_rush_four()` - 判断冲四
|
||||
- `_check_three_three()` - 检查三三禁手
|
||||
- `_check_four_four()` - 检查四四禁手
|
||||
- `_check_overline()` - 检查长连禁手
|
||||
|
||||
### 4. 状态管理
|
||||
- 使用`min(player1_id, player2_id)`作为数据库中的user_id
|
||||
- 在state_data中完整存储对战信息
|
||||
- 提供辅助方法查找用户当前参与的游戏
|
||||
|
||||
### 5. 路由注册
|
||||
在`routers/callback.py`的`handle_command()`函数中添加:
|
||||
```python
|
||||
if game_type == 'gomoku':
|
||||
from games.gomoku import GomokuGame
|
||||
game = GomokuGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
### 6. 指令解析
|
||||
在`utils/parser.py`的`CommandParser`类中添加gomoku指令识别
|
||||
|
||||
### 7. 配置更新
|
||||
在`config.py`中添加五子棋相关配置(如果需要)
|
||||
|
||||
# 当前执行步骤:"已完成所有实施步骤"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## [2025-10-28 17:18:21]
|
||||
- 已修改:
|
||||
- config.py - 添加gomoku配置
|
||||
- utils/parser.py - 添加gomoku指令映射
|
||||
- games/gomoku_logic.py - 创建五子棋逻辑模块(新文件)
|
||||
- games/gomoku.py - 创建五子棋游戏类(新文件)
|
||||
- routers/callback.py - 添加gomoku路由
|
||||
- games/base.py - 更新帮助信息和统计信息
|
||||
- 更改:完成五子棋游戏的完整实现,包括:
|
||||
- 群级游戏池管理(支持多轮对战并存)
|
||||
- 标准15x15棋盘
|
||||
- 完整的禁手规则(三三、四四、长连)
|
||||
- 坐标系统(A-O列,1-15行)
|
||||
- emoji棋盘渲染(⚫⚪➕)
|
||||
- 胜负判定
|
||||
- 战绩统计
|
||||
- 原因:实现用户需求的双人对战五子棋游戏
|
||||
- 阻碍因素:用户识别和显示格式错误
|
||||
- 状态:不成功
|
||||
|
||||
## [2025-10-28 17:36:07]
|
||||
- 已修改:
|
||||
- games/gomoku.py - 修复用户识别和显示格式
|
||||
- 更改:
|
||||
- 修复 `_parse_opponent()` 方法,使用正确的WPS @用户格式 `<at user_id="xxx"></at>` 进行解析
|
||||
- 修改所有用户显示,从 `@用户{user_id}` 改为 `<at user_id="{user_id}"></at>`,以正确显示用户的群名称
|
||||
- 涉及修改:开始游戏、落子、显示棋盘、认输、列出对战等所有用户显示位置
|
||||
- 原因:修复用户识别失败和显示错误的问题
|
||||
- 阻碍因素:用户识别仍然失败
|
||||
- 状态:不成功
|
||||
|
||||
## [2025-10-28 17:42:24]
|
||||
- 已修改:
|
||||
- games/gomoku.py - 改进用户ID解析和添加调试日志
|
||||
- routers/callback.py - 增强日志输出
|
||||
- 更改:
|
||||
- 改进 `_parse_opponent()` 方法,支持多种@用户格式(双引号、单引号、不同的标签格式)
|
||||
- 在 `handle()` 方法中添加详细的调试日志(command, args, action, opponent_id)
|
||||
- 改进错误提示,显示实际接收到的参数内容
|
||||
- 将 callback.py 中的消息内容日志级别从 DEBUG 改为 INFO,便于追踪
|
||||
- 原因:进一步诊断用户ID识别失败的问题,添加调试信息帮助定位问题
|
||||
- 阻碍因素:WPS callback不提供被@用户的ID信息
|
||||
- 状态:不成功
|
||||
|
||||
## [2025-10-28 17:55:00]
|
||||
- 已修改:
|
||||
- games/gomoku.py - 重构游戏发起机制,从@用户改为挑战-接受模式
|
||||
- games/base.py - 更新全局帮助信息
|
||||
- routers/callback.py - 添加完整callback数据日志
|
||||
- 更改:
|
||||
- **核心架构变更**:从"@用户发起对战"改为"挑战-接受"机制
|
||||
- 新增 `_create_challenge()` 方法 - 用户发起挑战
|
||||
- 新增 `_accept_challenge()` 方法 - 其他用户接受挑战
|
||||
- 新增 `_cancel_challenge()` 方法 - 取消自己的挑战
|
||||
- 删除 `_parse_opponent()` 方法(不再需要)
|
||||
- 删除 `_start_game()` 方法(由新方法替代)
|
||||
- 更新游戏池数据结构,添加 `challenges` 列表
|
||||
- 更新所有帮助信息和错误提示
|
||||
- 指令变更:
|
||||
- `.gomoku challenge` / `.gomoku start` - 发起挑战
|
||||
- `.gomoku accept` / `.gomoku join` - 接受挑战
|
||||
- `.gomoku cancel` - 取消挑战
|
||||
- 原因:WPS callback消息内容中@用户只是文本形式(如"@揭英飙"),不包含user_id,无法实现@用户发起对战
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## [2025-10-28 17:56:03]
|
||||
- 已修改:
|
||||
- games/gomoku_logic.py - 修复棋盘对齐问题
|
||||
- 更改:
|
||||
- 优化 `render_board()` 函数的格式化逻辑
|
||||
- 列标题:每个字母后面加一个空格,确保与棋子列对齐
|
||||
- 行号:调整前导空格,从 " {row_num} " 改为 "{row_num} "
|
||||
- 棋子:每个emoji后面加一个空格,行尾去除多余空格
|
||||
- 整体对齐:确保列标题、行号、棋子三者在Markdown代码块中正确对齐
|
||||
- 原因:修复用户反馈的棋盘文本对齐问题
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
# 最终审查
|
||||
(待完成后填写)
|
||||
|
||||
@@ -1,623 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-10-28_1_wps-bot-game.md
|
||||
创建于:2025-10-28_12:06:06
|
||||
创建者:揭英飙
|
||||
主分支:main
|
||||
任务分支:task/wps-bot-game_2025-10-28_1
|
||||
Yolo模式:On
|
||||
|
||||
# 任务描述
|
||||
开发基于WPS协作开放平台的自定义机器人游戏系统,实现多种互动小游戏功能,包括:
|
||||
1. 骰娘系统 - 支持多种骰子规则(基础掷骰、COC跑团、DND等)
|
||||
2. 猜数字游戏 - 经典的猜数字游戏
|
||||
3. 石头剪刀布 - 与机器人对战
|
||||
4. 抽签/占卜系统 - 每日运势、塔罗牌等
|
||||
5. 成语接龙 - 智能成语接龙
|
||||
6. 简单问答 - 脑筋急转弯、知识问答
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 技术栈
|
||||
- **后端框架**:FastAPI(现代化、异步支持)
|
||||
- **数据库**:SQLite(轻量级,适合小规模使用)
|
||||
- **Python版本**:使用conda环境liubai
|
||||
- **部署环境**:Ubuntu云服务器
|
||||
|
||||
## 核心配置
|
||||
- **Webhook URL**:https://xz.wps.cn/api/v1/webhook/send?key=da86927e491f2aef4b964223687c2c80
|
||||
- **消息限制**:20条/分钟,单条不超过5000字符
|
||||
- **Callback机制**:
|
||||
- GET验证:返回`{"result":"ok"}`
|
||||
- POST接收:接收chatid、creator、content、robot_key等参数
|
||||
|
||||
## WPS机器人API要点
|
||||
|
||||
### 消息类型
|
||||
1. **文本消息**(text)
|
||||
- 支持@人:`<at user_id="12345">姓名</at>`
|
||||
- @所有人:`<at user_id="-1">所有人</at>`
|
||||
|
||||
2. **Markdown消息**(markdown)
|
||||
- 支持标题、加粗、斜体、链接、列表等
|
||||
- 支持颜色:`<font color='#FF0000'>文字</font>`
|
||||
|
||||
3. **链接消息**(link)
|
||||
- 标题、文本、跳转URL、按钮文字
|
||||
|
||||
4. **卡片消息**(card)
|
||||
- 结构化展示
|
||||
- 注意:不支持回传型交互组件
|
||||
|
||||
### Callback交互流程
|
||||
```
|
||||
用户在群里@机器人 → WPS POST消息到Callback URL →
|
||||
服务器解析指令 → 调用游戏逻辑 → 通过Webhook URL回复消息
|
||||
```
|
||||
|
||||
## 开发策略
|
||||
- **分支开发**:每个游戏功能独立分支开发后合并
|
||||
- **模块化设计**:游戏逻辑独立模块,便于扩展
|
||||
- **配置化管理**:Webhook密钥通过配置文件管理
|
||||
- **简单实用**:小规模使用,不需要过度考虑安全性
|
||||
|
||||
# 分析
|
||||
|
||||
## 项目结构规划
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置文件
|
||||
├── requirements.txt # 依赖包
|
||||
├── .env # 环境变量(webhook密钥等)
|
||||
├── database.py # 数据库连接和模型
|
||||
├── models.py # 数据模型
|
||||
├── routers/ # API路由
|
||||
│ ├── webhook.py # Webhook回调处理
|
||||
│ └── callback.py # Callback接收处理
|
||||
├── games/ # 游戏模块
|
||||
│ ├── __init__.py
|
||||
│ ├── dice.py # 骰娘系统
|
||||
│ ├── guess_number.py # 猜数字
|
||||
│ ├── rps.py # 石头剪刀布
|
||||
│ ├── fortune.py # 抽签占卜
|
||||
│ ├── idiom.py # 成语接龙
|
||||
│ └── quiz.py # 问答游戏
|
||||
├── utils/ # 工具函数
|
||||
│ ├── message.py # 消息构造和发送
|
||||
│ ├── parser.py # 指令解析
|
||||
│ └── rate_limit.py # 限流控制
|
||||
└── data/ # 数据文件
|
||||
├── bot.db # SQLite数据库
|
||||
├── idioms.json # 成语数据
|
||||
└── quiz.json # 问答题库
|
||||
```
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 用户表(users)
|
||||
- user_id:WPS用户ID
|
||||
- username:用户名
|
||||
- created_at:首次使用时间
|
||||
- last_active:最后活跃时间
|
||||
|
||||
### 游戏状态表(game_states)
|
||||
- id:主键
|
||||
- chat_id:会话ID
|
||||
- user_id:用户ID
|
||||
- game_type:游戏类型(dice/guess/rps等)
|
||||
- state_data:游戏状态JSON
|
||||
- created_at:创建时间
|
||||
- updated_at:更新时间
|
||||
|
||||
### 游戏统计表(game_stats)
|
||||
- id:主键
|
||||
- user_id:用户ID
|
||||
- game_type:游戏类型
|
||||
- wins:胜利次数
|
||||
- losses:失败次数
|
||||
- draws:平局次数
|
||||
- total_plays:总游戏次数
|
||||
|
||||
## 指令系统设计
|
||||
|
||||
### 骰娘指令
|
||||
- `.r [XdY+Z]` - 掷骰子(如:.r 1d20+5)
|
||||
- `.r [XdY]` - 简单掷骰(如:.r 3d6)
|
||||
- `.rc [属性]` - COC检定
|
||||
- `.ra [技能]` - COC技能检定
|
||||
|
||||
### 猜数字
|
||||
- `.guess start` - 开始游戏
|
||||
- `.guess [数字]` - 猜测数字
|
||||
- `.guess stop` - 结束游戏
|
||||
|
||||
### 石头剪刀布
|
||||
- `.rps [石头/剪刀/布]` - 出拳
|
||||
- `.rps stats` - 查看战绩
|
||||
|
||||
### 抽签占卜
|
||||
- `.fortune` - 今日运势
|
||||
- `.tarot` - 塔罗占卜
|
||||
|
||||
### 成语接龙
|
||||
- `.idiom start` - 开始接龙
|
||||
- `.idiom [成语]` - 接成语
|
||||
|
||||
### 问答游戏
|
||||
- `.quiz` - 随机问题
|
||||
- `.quiz answer [答案]` - 回答问题
|
||||
|
||||
### 通用指令
|
||||
- `.help` - 帮助信息
|
||||
- `.stats` - 个人统计
|
||||
- `.about` - 关于机器人
|
||||
|
||||
## 核心技术实现要点
|
||||
|
||||
### 1. 消息接收与解析
|
||||
```python
|
||||
@app.post("/callback")
|
||||
async def receive_message(data: dict):
|
||||
content = data.get("content", "")
|
||||
chat_id = data.get("chatid")
|
||||
user_id = data.get("creator")
|
||||
|
||||
# 解析@机器人后的指令
|
||||
command = parse_command(content)
|
||||
|
||||
# 路由到对应游戏处理器
|
||||
result = await game_router(command, chat_id, user_id)
|
||||
|
||||
# 发送回复
|
||||
await send_message(result)
|
||||
|
||||
return {"result": "ok"}
|
||||
```
|
||||
|
||||
### 2. Webhook消息发送
|
||||
```python
|
||||
async def send_message(chat_id, message_type, content):
|
||||
url = "https://xz.wps.cn/api/v1/webhook/send?key=..."
|
||||
payload = {
|
||||
"msgtype": message_type,
|
||||
message_type: content
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, json=payload)
|
||||
return response
|
||||
```
|
||||
|
||||
### 3. 游戏状态管理
|
||||
- 使用SQLite存储游戏状态
|
||||
- 支持多会话并发
|
||||
- 游戏超时自动清理
|
||||
|
||||
### 4. 限流控制
|
||||
- 基于令牌桶算法
|
||||
- 防止触发20条/分钟限制
|
||||
- 消息队列缓冲
|
||||
|
||||
## 技术难点与解决方案
|
||||
|
||||
### 难点1:异步消息处理
|
||||
**问题**:用户发消息后需要快速响应
|
||||
**方案**:FastAPI异步处理+后台任务队列
|
||||
|
||||
### 难点2:游戏状态持久化
|
||||
**问题**:多用户多会话状态管理
|
||||
**方案**:SQLite+JSON字段存储灵活状态
|
||||
|
||||
### 难点3:指令解析
|
||||
**问题**:复杂的骰娘指令解析
|
||||
**方案**:正则表达式+状态机解析
|
||||
|
||||
### 难点4:消息限流
|
||||
**问题**:20条/分钟限制
|
||||
**方案**:令牌桶算法+消息队列
|
||||
|
||||
### 难点5:成语接龙算法
|
||||
**问题**:成语库匹配和接龙逻辑
|
||||
**方案**:预加载成语库+拼音索引
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案选择说明
|
||||
基于项目需求(小规模使用)和服务器资源限制(1GB内存+单核CPU),推荐采用**超轻量级单体架构**:
|
||||
|
||||
### 核心约束
|
||||
- **内存限制**:1GB总内存,预留给应用150-250MB
|
||||
- **CPU限制**:单核,避免多进程/多线程
|
||||
- **用户规模**:50-100个活跃用户
|
||||
- **并发能力**:5-10个同时请求
|
||||
|
||||
### 架构特点
|
||||
1. **FastAPI单体应用**(单worker模式):简单直接,资源占用低
|
||||
2. **按需加载游戏模块**:不预加载所有模块,运行时动态导入
|
||||
3. **SQLite标准库**:使用sqlite3而非SQLAlchemy ORM,零额外开销
|
||||
4. **懒加载数据**:成语库、题库等按需查询,不全量加载内存
|
||||
5. **严格并发控制**:限制同时处理请求数,避免内存爆炸
|
||||
|
||||
### 资源优化策略
|
||||
|
||||
#### 1. 内存优化
|
||||
- 使用sqlite3标准库,不用ORM(节省~50MB)
|
||||
- 不引入Redis(节省~150MB)
|
||||
- 游戏模块按需导入(节省~30MB)
|
||||
- 数据文件懒加载,不预加载成语库
|
||||
- 会话超时自动清理(30分钟)
|
||||
|
||||
#### 2. 存储优化
|
||||
- 成语库存SQLite带索引,按需查询
|
||||
- 或使用精简版成语库(500-1000个常用)
|
||||
- 或使用免费成语API(零存储)
|
||||
|
||||
#### 3. 并发优化
|
||||
- uvicorn单worker运行
|
||||
- 限制最大并发数:5-10
|
||||
- 关闭不必要的功能(Swagger文档等)
|
||||
|
||||
### 预估资源占用
|
||||
```
|
||||
FastAPI基础: 50MB
|
||||
游戏逻辑代码: 30MB
|
||||
SQLite连接: 10MB
|
||||
活跃会话数据: 30MB
|
||||
系统缓冲: 50MB
|
||||
-------------------
|
||||
总计: ~170MB
|
||||
剩余: ~830MB
|
||||
```
|
||||
|
||||
### 开发顺序(按优先级和资源消耗)
|
||||
|
||||
**Phase 1 - 核心框架**(main分支)
|
||||
1. FastAPI应用骨架(极简配置)
|
||||
2. Callback/Webhook路由
|
||||
3. SQLite数据库初始化(使用sqlite3)
|
||||
4. 消息工具函数
|
||||
5. 指令解析器基础框架
|
||||
|
||||
**Phase 2 - 无状态游戏**(优先开发,资源占用低)
|
||||
1. **骰娘分支**(feature/dice-game)⭐⭐⭐⭐⭐
|
||||
- 基础掷骰(.r XdY)
|
||||
- 带修正的掷骰(.r XdY+Z)
|
||||
- 多次掷骰
|
||||
|
||||
2. **石头剪刀布分支**(feature/rps)⭐⭐⭐⭐
|
||||
- 基础对战逻辑
|
||||
- 简单战绩统计(可选)
|
||||
|
||||
3. **运势占卜分支**(feature/fortune)⭐⭐⭐⭐
|
||||
- 今日运势(基于日期seed)
|
||||
- 简单塔罗牌
|
||||
|
||||
**Phase 3 - 简单有状态游戏**
|
||||
4. **猜数字分支**(feature/guess-number)⭐⭐⭐
|
||||
- 游戏逻辑
|
||||
- 状态管理(极简)
|
||||
|
||||
5. **问答分支**(feature/quiz)⭐⭐
|
||||
- 小型题库(10-20题)
|
||||
- 答题逻辑
|
||||
|
||||
**Phase 4 - 可选高级功能**(视资源情况)
|
||||
6. **成语接龙分支**(feature/idiom)⭐
|
||||
- 使用SQLite存储精简成语库
|
||||
- 或使用免费API
|
||||
- 基础接龙算法
|
||||
|
||||
### 部署方案
|
||||
1. Ubuntu服务器配置Python环境
|
||||
2. 使用systemd管理FastAPI服务
|
||||
3. Nginx反向代理(如需HTTPS)
|
||||
4. 日志监控和自动重启
|
||||
|
||||
# 当前执行步骤:"完成核心开发"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## [2025-10-28_12:20] 核心开发完成
|
||||
|
||||
### 已完成的文件列表
|
||||
|
||||
**配置和基础**
|
||||
- ✅ config.py - 配置管理模块
|
||||
- ✅ requirements.txt - Python依赖
|
||||
- ✅ env.example - 环境变量模板
|
||||
- ✅ .gitignore - Git忽略文件
|
||||
|
||||
**核心模块 (core/)**
|
||||
- ✅ database.py - SQLite数据库操作(使用标准库sqlite3)
|
||||
- ✅ models.py - Pydantic数据模型
|
||||
- ✅ middleware.py - 并发限制中间件
|
||||
|
||||
**路由模块 (routers/)**
|
||||
- ✅ callback.py - Callback接收和指令路由
|
||||
- ✅ health.py - 健康检查和系统统计
|
||||
|
||||
**工具模块 (utils/)**
|
||||
- ✅ message.py - WPS消息构造和发送
|
||||
- ✅ parser.py - 指令解析器
|
||||
- ✅ rate_limit.py - 令牌桶限流器
|
||||
|
||||
**游戏模块 (games/)**
|
||||
- ✅ base.py - 游戏基类和帮助系统
|
||||
- ✅ dice.py - 骰娘系统(支持XdY+Z格式)
|
||||
- ✅ rps.py - 石头剪刀布(含战绩统计)
|
||||
- ✅ fortune.py - 运势占卜(每日运势+塔罗牌)
|
||||
- ✅ guess.py - 猜数字游戏(1-100,10次机会)
|
||||
- ✅ quiz.py - 问答游戏(15道题,3次机会)
|
||||
|
||||
**数据文件 (data/)**
|
||||
- ✅ fortunes.json - 运势和塔罗牌数据
|
||||
- ✅ quiz.json - 问答题库
|
||||
|
||||
**主应用**
|
||||
- ✅ app.py - FastAPI主应用(含生命周期管理)
|
||||
|
||||
**部署配置**
|
||||
- ✅ README.md - 完整项目文档
|
||||
- ✅ deploy/systemd/wps-bot.service - systemd服务配置
|
||||
|
||||
### 已实现的功能
|
||||
|
||||
**1. 骰娘系统** ⭐⭐⭐⭐⭐
|
||||
- [x] 基础掷骰(.r XdY)
|
||||
- [x] 带修正掷骰(.r XdY+Z)
|
||||
- [x] 大成功/大失败识别
|
||||
- [x] Markdown格式化输出
|
||||
|
||||
**2. 石头剪刀布** ⭐⭐⭐⭐
|
||||
- [x] 基础对战逻辑
|
||||
- [x] 战绩统计系统
|
||||
- [x] 胜率计算
|
||||
- [x] 多种输入方式(中英文+表情)
|
||||
|
||||
**3. 运势占卜** ⭐⭐⭐⭐
|
||||
- [x] 每日运势(基于日期seed)
|
||||
- [x] 塔罗牌占卜
|
||||
- [x] 幸运数字和颜色
|
||||
- [x] 懒加载数据文件
|
||||
|
||||
**4. 猜数字游戏** ⭐⭐⭐
|
||||
- [x] 游戏状态管理
|
||||
- [x] 智能提示系统
|
||||
- [x] 范围缩小提示
|
||||
- [x] 10次机会限制
|
||||
|
||||
**5. 问答游戏** ⭐⭐
|
||||
- [x] 15道题的题库
|
||||
- [x] 关键词智能匹配
|
||||
- [x] 3次回答机会
|
||||
- [x] 提示系统
|
||||
|
||||
**核心系统**
|
||||
- [x] WPS Callback验证和接收
|
||||
- [x] 指令解析和路由
|
||||
- [x] 消息构造和发送(文本/Markdown)
|
||||
- [x] 限流控制(20条/分钟)
|
||||
- [x] 并发限制(5个同时请求)
|
||||
- [x] 数据库连接和管理
|
||||
- [x] 用户管理和统计
|
||||
- [x] 游戏状态持久化
|
||||
- [x] 会话自动清理(30分钟)
|
||||
- [x] 全局异常处理
|
||||
- [x] 日志系统
|
||||
|
||||
### 技术特性
|
||||
|
||||
**资源优化**
|
||||
- ✅ 使用sqlite3标准库(无ORM开销)
|
||||
- ✅ 游戏模块按需导入(不预加载)
|
||||
- ✅ 数据文件懒加载
|
||||
- ✅ 单worker模式
|
||||
- ✅ 严格并发控制
|
||||
- ✅ 预估内存占用:150-250MB
|
||||
|
||||
**代码质量**
|
||||
- ✅ 完整的类型提示
|
||||
- ✅ 详细的文档字符串
|
||||
- ✅ 错误处理和日志
|
||||
- ✅ 模块化设计
|
||||
- ✅ 清晰的项目结构
|
||||
|
||||
### 已完成的清单项
|
||||
|
||||
**阶段1:基础框架**
|
||||
- [x] 1-4. 创建项目结构和基础文件
|
||||
- [x] 5. 编写config.py配置管理
|
||||
- [x] 6-7. 编写database.py和初始化表结构
|
||||
- [x] 8. 编写models.py数据模型
|
||||
- [x] 9. 编写middleware.py中间件
|
||||
- [x] 10. 创建FastAPI主应用app.py
|
||||
|
||||
**阶段2:消息处理**
|
||||
- [x] 11. 编写message.py消息工具
|
||||
- [x] 12. 编写parser.py指令解析器
|
||||
- [x] 13. 编写rate_limit.py限流控制
|
||||
- [x] 14. 编写callback.py路由
|
||||
- [x] 15. 编写health.py健康检查
|
||||
- [x] 16. 编写base.py游戏基类
|
||||
- [x] 17. 实现帮助指令处理
|
||||
|
||||
**阶段3:骰娘系统**
|
||||
- [x] 18-23. 完整实现骰娘模块
|
||||
|
||||
**阶段4:石头剪刀布**
|
||||
- [x] 24-28. 完整实现石头剪刀布模块
|
||||
|
||||
**阶段5:运势占卜**
|
||||
- [x] 29-33. 完整实现运势占卜模块
|
||||
|
||||
**阶段6:猜数字**
|
||||
- [x] 34-38. 完整实现猜数字模块
|
||||
|
||||
**阶段7:问答游戏**
|
||||
- [x] 39-43. 完整实现问答模块
|
||||
|
||||
**阶段8:部署准备**
|
||||
- [x] 44. 编写README.md文档
|
||||
- [x] 45. 创建systemd服务配置
|
||||
- [ ] 46-47. 本地测试(待进行)
|
||||
- [ ] 48-51. 服务器部署(待用户进行)
|
||||
|
||||
### 变更说明
|
||||
- 所有功能按照计划实施
|
||||
- 使用sqlite3标准库替代SQLAlchemy(节省内存)
|
||||
- 游戏模块全部实现懒加载(节省内存)
|
||||
- 数据文件全部实现按需加载(节省内存)
|
||||
- 严格遵守资源限制(1GB内存+单核CPU)
|
||||
|
||||
### 阻碍因素
|
||||
- 无
|
||||
|
||||
### 状态
|
||||
- ✅ 成功
|
||||
|
||||
## [2025-10-28_12:51] 本地测试完成
|
||||
|
||||
### 测试环境
|
||||
- 操作系统: Windows 10
|
||||
- Python环境: conda环境liubai
|
||||
- 测试方式: 本地启动FastAPI应用
|
||||
|
||||
### 测试结果
|
||||
|
||||
**接口测试** ✅ 全部通过
|
||||
- GET / - 200 OK (API运行中)
|
||||
- GET /health - 200 OK (健康检查)
|
||||
- GET /stats - 200 OK (系统统计)
|
||||
- GET /api/callback - 200 OK (Callback验证)
|
||||
- POST /api/callback - 200 OK (消息接收)
|
||||
|
||||
**游戏功能测试** ✅ 全部通过
|
||||
- 骰娘系统 (.r 1d20) - 正常处理
|
||||
- 石头剪刀布 (.rps 石头) - 正常处理
|
||||
- 运势占卜 (.fortune) - 正常处理
|
||||
- 猜数字游戏 (.guess start) - 正常处理并创建游戏状态
|
||||
|
||||
**资源使用情况** 🎯 远超预期
|
||||
- 内存占用: 61.32 MB(预算250MB,实际节省75%!)
|
||||
- CPU占用: 0.0%
|
||||
- 线程数: 4个
|
||||
- 数据库: 正常工作,用户记录正确
|
||||
|
||||
**数据持久化** ✅ 正常
|
||||
- 用户管理: 1个用户成功记录
|
||||
- 游戏状态: 1个活跃游戏(猜数字)
|
||||
- 数据库文件: data/bot.db 成功创建
|
||||
|
||||
### 性能亮点
|
||||
1. **内存占用极低**: 61MB vs 预算250MB(节省189MB)
|
||||
2. **启动速度快**: 应用3秒内完成启动
|
||||
3. **响应速度快**: 所有请求<100ms
|
||||
4. **模块懒加载**: 按需导入工作正常
|
||||
5. **并发控制**: 中间件正常工作
|
||||
|
||||
### 完成清单项
|
||||
- [x] 46. 本地语法检查
|
||||
- [x] 47. 本地功能测试
|
||||
|
||||
### 待进行项
|
||||
- [ ] 48. 准备服务器环境(用户操作)
|
||||
- [ ] 49. 部署到Ubuntu服务器(用户操作)
|
||||
- [ ] 50. 配置systemd服务(用户操作)
|
||||
- [ ] 51. 启动服务并监控(用户操作)
|
||||
|
||||
### 测试结论
|
||||
✅ **所有核心功能正常,性能表现优异,可以部署到生产环境**
|
||||
|
||||
### 状态
|
||||
- ✅ 本地测试成功
|
||||
|
||||
# 最终审查
|
||||
|
||||
## 完成度统计
|
||||
|
||||
**文件数量**: 25个
|
||||
**代码行数**: ~2500行
|
||||
**完成进度**: 47/51 (92%)
|
||||
|
||||
**已完成**:
|
||||
- ✅ 阶段1: 基础框架(10/10项)
|
||||
- ✅ 阶段2: 消息处理(7/7项)
|
||||
- ✅ 阶段3: 骰娘系统(6/6项)
|
||||
- ✅ 阶段4: 石头剪刀布(5/5项)
|
||||
- ✅ 阶段5: 运势占卜(5/5项)
|
||||
- ✅ 阶段6: 猜数字(5/5项)
|
||||
- ✅ 阶段7: 问答游戏(5/5项)
|
||||
- ✅ 阶段8: 部署准备(4/4项)
|
||||
- ✅ 本地测试(2/2项)
|
||||
|
||||
**待用户完成**:
|
||||
- ⏳ 服务器部署(4项)
|
||||
|
||||
## 技术实现评估
|
||||
|
||||
### 架构设计 ⭐⭐⭐⭐⭐
|
||||
- 超轻量级单体架构
|
||||
- 模块化设计,易于扩展
|
||||
- 按需加载,资源占用极低
|
||||
|
||||
### 代码质量 ⭐⭐⭐⭐⭐
|
||||
- 完整的类型提示
|
||||
- 详细的文档字符串
|
||||
- 全面的错误处理
|
||||
- 清晰的日志系统
|
||||
|
||||
### 性能表现 ⭐⭐⭐⭐⭐
|
||||
- 内存: 61MB(预算250MB,超额完成175%)
|
||||
- 响应速度: <100ms
|
||||
- 并发支持: 5-10请求
|
||||
- 启动速度: 3秒
|
||||
|
||||
### 功能完整性 ⭐⭐⭐⭐⭐
|
||||
- 5个游戏模块全部实现
|
||||
- WPS接口完整对接
|
||||
- 用户管理系统完善
|
||||
- 游戏状态持久化正常
|
||||
|
||||
## 偏差分析
|
||||
|
||||
### 与计划的对比
|
||||
✅ **完全符合计划**,无重大偏差
|
||||
|
||||
细微调整:
|
||||
1. 添加psutil依赖(用于系统监控)
|
||||
2. 内存占用远低于预期(好的偏差)
|
||||
|
||||
## 部署建议
|
||||
|
||||
### 服务器要求
|
||||
- 操作系统: Ubuntu 20.04+
|
||||
- Python: 3.10+
|
||||
- 内存: 1GB(实际只需200MB)
|
||||
- CPU: 单核即可
|
||||
|
||||
### 部署步骤
|
||||
1. 上传项目到服务器
|
||||
2. 安装依赖: `pip install -r requirements.txt`
|
||||
3. 配置Webhook URL
|
||||
4. 使用systemd配置服务
|
||||
5. 在WPS中配置Callback URL
|
||||
6. 启动服务并测试
|
||||
|
||||
### 监控要点
|
||||
- 内存使用: 应<150MB
|
||||
- 响应时间: 应<500ms
|
||||
- 限流状态: 20条/分钟
|
||||
- 数据库大小: 定期清理
|
||||
|
||||
## 最终结论
|
||||
|
||||
✅ **项目开发完成,测试通过,可以部署**
|
||||
|
||||
本项目成功实现了:
|
||||
1. 资源受限环境下的高效运行(1GB内存)
|
||||
2. 5个完整的游戏功能
|
||||
3. 完善的WPS接口对接
|
||||
4. 优秀的代码质量和可维护性
|
||||
5. 详细的文档和部署指南
|
||||
|
||||
**推荐操作**: 立即部署到生产环境,开始使用!
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
# 背景
|
||||
文件名: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 的问题,以及@前缀处理不完整的问题
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
# 最终审查
|
||||
[等待实施]
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
# 背景
|
||||
文件名: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系统中。
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-10-29_3_ai_chat.md
|
||||
创建于:2025-10-29_23:32:40
|
||||
创建者:user
|
||||
主分支:main
|
||||
任务分支:task/ai_chat_2025-10-29_3
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在本项目中新增一个AI对话功能,使用llama_index构建,服务商为本地部署的ollama。
|
||||
|
||||
这个智能体将拥有足够长的上下文(超过30轮对话历史),能够同时与不同的用户展开交流。例如用户A提问后用户B进行补充,智能体将通过时间间隔判断机制来决定何时进行回答。
|
||||
|
||||
## 核心需求
|
||||
1. **显式指令触发**:使用 `.ai <问题>` 指令触发AI对话
|
||||
2. **配置指令**:使用 `.aiconfig` 指令配置Ollama服务地址、端口和模型名称
|
||||
3. **时间间隔判断**:智能体通过时间间隔判断是否需要回答(固定10秒等待窗口)
|
||||
4. **长上下文管理**:保留超过30轮对话历史
|
||||
5. **多用户对话支持**:同一chat_id下不同用户的消息能够被正确识别和处理
|
||||
|
||||
## 技术方案决策(已确定)
|
||||
1. **延迟任务机制**:使用 asyncio 的延迟任务(方案三)
|
||||
- 每个 chat_id 维护独立的延迟任务句柄
|
||||
- 使用全局字典存储任务映射
|
||||
- 收到新消息时取消旧任务并创建新任务
|
||||
|
||||
2. **上下文管理**:使用 llama_index 的 ChatMemoryBuffer(策略A)
|
||||
- 设置足够的 token_limit 确保保留30+轮对话
|
||||
- 按 chat_id 独立维护 ChatEngine 实例
|
||||
|
||||
3. **多用户识别**:消息角色映射 + 系统提示(方案C)
|
||||
- 将不同用户映射为不同角色(如"用户1"、"用户2")
|
||||
- 在系统提示中明确告知存在多用户场景
|
||||
- ChatMemoryBuffer 中使用角色区分不同用户
|
||||
|
||||
4. **等待窗口**:固定10秒窗口(变体1)
|
||||
- 收到消息后等待10秒
|
||||
- 等待期间有新消息则重新计时
|
||||
|
||||
5. **配置管理**:使用单独的JSON文件
|
||||
- 配置存储在 `data/ai_config.json`
|
||||
- 全局单一配置(服务器级别,非chat级别)
|
||||
- 通过 `.aiconfig` 指令修改配置并保存到文件
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBot/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/ # 核心模块
|
||||
│ ├── database.py # 数据库操作
|
||||
│ ├── models.py # 数据模型
|
||||
│ └── middleware.py # 中间件
|
||||
├── routers/ # 路由模块
|
||||
│ ├── callback.py # 回调处理
|
||||
│ └── health.py # 健康检查
|
||||
├── games/ # 游戏模块
|
||||
│ ├── base.py # 游戏基类
|
||||
│ └── ... # 其他游戏
|
||||
├── utils/ # 工具模块
|
||||
│ ├── message.py # 消息发送
|
||||
│ ├── parser.py # 指令解析
|
||||
│ └── rate_limit.py # 限流控制
|
||||
└── data/ # 数据文件
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
- FastAPI:Web框架
|
||||
- SQLite:数据存储
|
||||
- llama-index-core:AI对话框架核心
|
||||
- llama-index-llms-ollama:Ollama LLM集成
|
||||
- Ollama:本地LLM服务
|
||||
|
||||
# 分析
|
||||
|
||||
## 现有架构
|
||||
1. **指令处理流程**:
|
||||
- 消息通过 `/api/callback` 接收
|
||||
- `CommandParser` 解析指令,只处理以 `.` 开头的命令
|
||||
- 非指令消息会被忽略
|
||||
- 指令分发到对应的游戏处理器
|
||||
|
||||
2. **状态管理**:
|
||||
- 游戏状态存储在 `game_states` 表
|
||||
- 使用 `(chat_id, user_id, game_type)` 作为联合主键
|
||||
- 对于群组共享状态,使用 `user_id=0`(如成语接龙)
|
||||
|
||||
3. **异步任务**:
|
||||
- 已有 `periodic_cleanup` 后台清理任务示例
|
||||
- 使用 `asyncio.create_task` 和 `asyncio.sleep` 实现
|
||||
|
||||
## 关键技术挑战
|
||||
|
||||
### 1. 延迟回答机制
|
||||
需要实现一个基于时间间隔的判断机制:
|
||||
- 收到 `.ai` 指令时,将消息加入等待队列
|
||||
- 设置一个等待窗口(例如5-10秒)
|
||||
- 如果在等待窗口内有新消息,重新计时
|
||||
- 等待窗口结束后,如果没有新消息,生成回答
|
||||
- 需要在 `chat_id` 级别维护等待队列和延迟任务
|
||||
|
||||
### 2. 长上下文管理
|
||||
- 使用 llama_index 的 `ChatMemoryBuffer` 管理对话历史
|
||||
- 确保超过30轮对话历史能够被保留
|
||||
- 对话历史需要按 `chat_id` 独立存储
|
||||
- 对话历史中需要包含用户ID信息,以便区分不同用户
|
||||
|
||||
### 3. Ollama配置管理
|
||||
- 使用全局单一配置(服务器级别)
|
||||
- 配置存储在 `data/ai_config.json` 文件中
|
||||
- 配置包括:服务地址、端口、模型名称
|
||||
- 通过 `.aiconfig` 指令修改配置并持久化到文件
|
||||
- 配置需要有默认值(localhost:11434,默认模型需指定)
|
||||
|
||||
### 4. 多用户对话识别
|
||||
- 对话历史中需要记录每条消息的发送者(user_id)
|
||||
- 生成回复时,需要识别上下文中的不同用户
|
||||
- 回复格式可以考虑使用 @用户 的方式
|
||||
|
||||
### 5. 依赖管理
|
||||
- 需要添加 llama-index-core 和相关依赖
|
||||
- 需要确保与现有代码库的兼容性
|
||||
- 考虑资源占用(内存、CPU)
|
||||
|
||||
## 数据结构设计
|
||||
|
||||
### AI对话状态数据结构
|
||||
对话状态由 llama_index 的 ChatMemoryBuffer 管理,存储在内存中。
|
||||
需要存储的额外信息:
|
||||
|
||||
```python
|
||||
# 存储在 game_states 表中的 state_data
|
||||
{
|
||||
"user_mapping": { # 用户ID到角色名称的映射
|
||||
"123456": "用户1",
|
||||
"789012": "用户2",
|
||||
...
|
||||
},
|
||||
"user_count": 2 # 当前对话中的用户数量
|
||||
}
|
||||
```
|
||||
|
||||
### 配置数据结构(存储在 data/ai_config.json)
|
||||
```json
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 11434,
|
||||
"model": "llama3.1"
|
||||
}
|
||||
```
|
||||
|
||||
### 数据库扩展
|
||||
使用 `game_states` 表存储用户映射信息:
|
||||
- `chat_id`: 会话ID
|
||||
- `user_id`: 0(表示群组级别)
|
||||
- `game_type`: "ai_chat"
|
||||
- `state_data`: JSON格式的用户映射信息
|
||||
|
||||
注意:对话历史由 ChatMemoryBuffer 在内存中管理,不持久化到数据库。
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. 创建一个新的游戏模块 `games/ai_chat.py`,继承 `BaseGame`
|
||||
2. 使用 `game_states` 表存储用户映射信息(用户ID到角色名称的映射)
|
||||
3. 使用全局字典维护每个 `chat_id` 的延迟任务句柄
|
||||
4. 使用全局字典维护每个 `chat_id` 的 ChatEngine 实例和待处理消息队列
|
||||
5. 使用 `data/ai_config.json` 存储 Ollama 全局配置
|
||||
6. 使用 llama_index 的 ChatMemoryBuffer 管理对话上下文(内存中)
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 1. 指令注册
|
||||
在 `utils/parser.py` 中添加:
|
||||
- `.ai`: 触发AI对话
|
||||
- `.aiconfig`: 配置Ollama参数
|
||||
|
||||
### 2. AI对话模块 (`games/ai_chat.py`)
|
||||
- `handle()`: 主处理函数,处理 `.ai` 和 `.aiconfig` 指令
|
||||
- `_handle_ai()`: 处理AI对话请求
|
||||
- 将消息加入等待队列
|
||||
- 取消旧的延迟任务(如果存在)
|
||||
- 创建新的延迟任务(10秒后执行)
|
||||
- `_handle_config()`: 处理配置请求
|
||||
- 解析配置参数(host, port, model)
|
||||
- 更新 `data/ai_config.json` 文件
|
||||
- 返回配置确认消息
|
||||
- `_add_to_queue()`: 将消息加入等待队列(按 chat_id 组织)
|
||||
- `_delayed_response()`: 延迟回答任务(内部异步函数)
|
||||
- 等待10秒后执行
|
||||
- 检查队列并生成回答
|
||||
- 处理任务取消异常
|
||||
- `_generate_response()`: 使用LLM生成回答
|
||||
- 获取或创建 ChatEngine 实例
|
||||
- 获取用户角色映射
|
||||
- 将队列中的消息按用户角色格式化
|
||||
- 调用 ChatEngine.chat() 生成回答
|
||||
- 更新 ChatMemoryBuffer
|
||||
- `_get_chat_engine()`: 获取或创建ChatEngine实例
|
||||
- 检查全局字典中是否已存在
|
||||
- 不存在则创建新的 ChatEngine,配置 ChatMemoryBuffer
|
||||
- 设置系统提示(告知多用户场景)
|
||||
- `_get_user_role()`: 获取用户角色名称(创建或获取映射)
|
||||
- `_load_config()`: 从 JSON 文件加载配置
|
||||
- `_save_config()`: 保存配置到 JSON 文件
|
||||
|
||||
### 3. 延迟任务管理
|
||||
- 使用全局字典 `_pending_tasks` 存储每个 `chat_id` 的延迟任务句柄
|
||||
- 使用全局字典 `_message_queues` 存储每个 `chat_id` 的待处理消息队列
|
||||
- 使用全局字典 `_chat_engines` 存储每个 `chat_id` 的 ChatEngine 实例
|
||||
- 新消息到达时,取消旧任务(调用 task.cancel())并创建新任务
|
||||
- 使用 `asyncio.create_task` 和 `asyncio.sleep(10)` 实现固定10秒延迟
|
||||
- 处理 `asyncio.CancelledError` 异常,避免任务取消时的错误日志
|
||||
|
||||
### 4. 用户角色映射机制
|
||||
- 为每个 chat_id 维护用户ID到角色名称的映射(如"用户1"、"用户2")
|
||||
- 映射信息存储在 `game_states` 表中(chat_id, user_id=0, game_type='ai_chat')
|
||||
- 首次出现的用户自动分配角色名称(按出现顺序)
|
||||
- 在将消息添加到 ChatMemoryBuffer 时使用角色名称作为消息角色
|
||||
- 系统提示中包含:"这是一个多用户对话场景,不同用户的发言会用不同的角色标识。你需要理解不同用户的发言内容,并根据上下文给出合适的回复。"
|
||||
|
||||
### 4. 依赖添加
|
||||
在 `requirements.txt` 中添加:
|
||||
```
|
||||
llama-index-core>=0.10.0
|
||||
llama-index-llms-ollama>=0.1.0
|
||||
```
|
||||
|
||||
### 5. 路由注册
|
||||
在 `routers/callback.py` 的 `handle_command()` 中添加AI对话处理分支
|
||||
|
||||
### 6. 帮助信息更新
|
||||
在 `games/base.py` 的 `get_help_message()` 中添加AI对话帮助
|
||||
|
||||
## 时间间隔判断逻辑(固定10秒窗口)
|
||||
1. **默认等待窗口**:10秒(固定)
|
||||
2. **收到 `.ai` 指令时**:
|
||||
- 提取消息内容(去除 `.ai` 前缀)
|
||||
- 获取用户ID和chat_id
|
||||
- 将消息(用户ID + 内容)加入该 `chat_id` 的等待队列
|
||||
- 如果有待处理的延迟任务(检查 `_pending_tasks[chat_id]`),取消它
|
||||
- 创建新的延迟任务(`asyncio.create_task(_delayed_response(chat_id))`)
|
||||
- 将任务句柄存储到 `_pending_tasks[chat_id]`
|
||||
3. **在等待窗口内收到新消息**(无论是否是指令):
|
||||
- 如果新消息也是 `.ai` 指令:
|
||||
- 将新消息加入队列
|
||||
- 取消当前延迟任务(`task.cancel()`)
|
||||
- 创建新的延迟任务(重新计时10秒)
|
||||
- 如果新消息不是指令,但chat_id在等待队列中:
|
||||
- 可以考虑忽略,或也加入队列(根据需求决定)
|
||||
4. **等待窗口结束(延迟任务执行)**:
|
||||
- 检查队列中是否有消息
|
||||
- 如果有,获取该 chat_id 的 ChatEngine 和用户映射
|
||||
- 将队列中的消息按用户角色格式化后添加到 ChatMemoryBuffer
|
||||
- 调用 ChatEngine.chat() 生成回答
|
||||
- 清空队列
|
||||
- 从 `_pending_tasks` 中移除任务句柄
|
||||
|
||||
## 配置文件管理(data/ai_config.json)
|
||||
- 文件结构:
|
||||
```json
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 11434,
|
||||
"model": "llama3.1"
|
||||
}
|
||||
```
|
||||
- 首次加载时如果文件不存在,创建默认配置
|
||||
- 通过 `.aiconfig` 指令修改配置时,实时保存到文件
|
||||
- ChatEngine 创建时从配置文件加载配置
|
||||
|
||||
# 当前执行步骤:"4. 执行模式 - 代码实施完成并测试通过"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## [2025-10-29_23:55:08] 执行阶段完成
|
||||
- 已修改:
|
||||
- requirements.txt:添加 llama-index-core 和 llama-index-llms-ollama 依赖
|
||||
- data/ai_config.json:创建默认配置文件
|
||||
- utils/parser.py:添加 .ai 和 .aiconfig 指令映射和解析逻辑
|
||||
- games/ai_chat.py:创建完整的 AI 对话模块实现
|
||||
- routers/callback.py:添加 ai_chat 处理分支
|
||||
- games/base.py:添加 AI 对话帮助信息
|
||||
- 更改:
|
||||
- 实现了基于 llama_index 和 Ollama 的 AI 对话功能
|
||||
- 实现了固定10秒等待窗口的延迟回答机制
|
||||
- 实现了用户角色映射和长上下文管理
|
||||
- 实现了配置文件的 JSON 存储和管理
|
||||
- 原因:按照计划实施 AI 对话功能的所有核心组件
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## [2025-10-30_00:56:44] 功能优化和问题修复
|
||||
- 已修改:
|
||||
- games/ai_chat.py:优化错误处理和用户体验
|
||||
1. 移除收到消息后的确认回复(静默处理)
|
||||
2. 修复转义字符警告(SyntaxWarning)
|
||||
3. 改进错误处理,提供详细的调试信息和排查步骤
|
||||
4. 添加超时设置(120秒)
|
||||
5. 针对NPS端口转发的特殊错误提示
|
||||
- 更改:
|
||||
- 优化了错误提示信息,包含当前配置、测试命令和详细排查步骤
|
||||
- 专门针对NPS端口转发场景添加了Ollama监听地址配置说明
|
||||
- 改进了连接错误的诊断能力
|
||||
- 原因:根据实际使用中发现的问题进行优化
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## [2025-10-30_01:10:05] 系统提示词持久化和功能完善
|
||||
- 已修改:
|
||||
- games/ai_chat.py:
|
||||
1. 实现系统提示词的持久化存储(保存到配置文件)
|
||||
2. 添加 `_get_default_system_prompt()` 方法定义默认系统提示词
|
||||
3. 添加 `_get_system_prompt()` 方法从配置文件加载系统提示词
|
||||
4. 更新系统提示词内容,明确AI身份和职责
|
||||
5. 在系统提示词中包含完整的机器人功能列表和指引
|
||||
- 更改:
|
||||
- 系统提示词现在会保存到 `data/ai_config.json` 文件中
|
||||
- 服务重启后系统提示词会自动从配置文件加载,保持长期记忆
|
||||
- AI助手能够了解自己的身份和所有机器人功能,可以主动指引用户
|
||||
- 系统提示词包含了完整的13个功能模块介绍和回复指南
|
||||
- 原因:实现系统提示词的长期记忆,让AI能够始终记住自己的身份和职责
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 最终审查
|
||||
|
||||
## 实施总结
|
||||
✅ 所有计划功能已成功实施并通过测试
|
||||
|
||||
### 核心功能实现
|
||||
1. ✅ AI对话系统基于 llama_index + Ollama 构建
|
||||
2. ✅ 显式指令触发(`.ai <问题>`)
|
||||
3. ✅ 配置指令(`.aiconfig`)支持动态配置Ollama服务
|
||||
4. ✅ 固定10秒等待窗口的延迟回答机制
|
||||
5. ✅ 用户角色映射和长上下文管理(30+轮对话)
|
||||
6. ✅ 配置文件持久化存储
|
||||
7. ✅ 系统提示词持久化存储(新增)
|
||||
8. ✅ 完善的错误处理和调试信息
|
||||
|
||||
### 文件修改清单
|
||||
- ✅ requirements.txt - 添加依赖
|
||||
- ✅ data/ai_config.json - 配置文件(包含系统提示词)
|
||||
- ✅ utils/parser.py - 指令解析
|
||||
- ✅ games/ai_chat.py - AI对话模块完整实现
|
||||
- ✅ routers/callback.py - 路由注册
|
||||
- ✅ games/base.py - 帮助信息更新
|
||||
|
||||
### 技术特性
|
||||
- ✅ 多用户对话支持
|
||||
- ✅ 延迟任务管理(asyncio)
|
||||
- ✅ ChatMemoryBuffer长上下文管理
|
||||
- ✅ JSON配置文件管理
|
||||
- ✅ NPS端口转发支持
|
||||
- ✅ 详细的错误诊断和排查指南
|
||||
|
||||
### 测试状态
|
||||
- ✅ 功能测试通过
|
||||
- ✅ Ollama服务连接测试通过
|
||||
- ✅ NPS端口转发配置测试通过
|
||||
- ✅ 系统提示词持久化测试通过
|
||||
|
||||
## 实施与计划匹配度
|
||||
实施与计划完全匹配 ✅
|
||||
|
||||
|
||||
## 补充分析:Markdown 渲染与发送通道(2025-10-30)
|
||||
|
||||
### 现状观察
|
||||
|
||||
- `routers/callback.py` 中仅当 `response_text.startswith('#')` 时才通过 `send_markdown()` 发送,否则使用 `send_text()`。这意味着即使 AI 返回了合法的 Markdown,但不以 `#` 开头(例如代码块、列表、表格、普通段落等),也会被按纯文本通道发送,导致下游(WPS 侧)不进行 Markdown 渲染。
|
||||
- `games/ai_chat.py` 的 `_generate_response()` 直接返回 `str(response)`,未对内容类型进行标注或判定,上层仅依赖首字符为 `#` 的启发式判断来选择发送通道。
|
||||
- `utils/message.py` 已具备 `send_markdown()` 与 `send_text()` 两种发送方式,对应 `{"msgtype":"markdown"}` 与 `{"msgtype":"text"}` 消息结构;当前缺少自动识别 Markdown 的逻辑。
|
||||
|
||||
### 影响
|
||||
|
||||
- 当 AI 返回包含 Markdown 元素但非标题(不以 `#` 开头)的内容时,用户端看到的是未渲染的原始 Markdown 文本,表现为“格式不能成功排版”。
|
||||
|
||||
### 待确认问题(不含解决方案,需产品/实现口径)
|
||||
|
||||
1. 目标平台(WPS 机器人)对 Markdown 的要求是否仅需 `msgtype=markdown` 即可渲染?是否存在必须以标题开头的限制?
|
||||
2. 期望策略:
|
||||
- 是否希望“.ai 的所有回复”统一走 Markdown 通道?
|
||||
- 还是需要基于 Markdown 特征进行判定(如代码块、列表、链接、表格、行内格式等)?
|
||||
3. 兼容性:若统一改为 Markdown 通道,是否会影响既有纯文本展示(例如换行、转义、表情)?
|
||||
4. 其他指令模块是否也可能返回 Markdown?若有,是否一并纳入同一策略?
|
||||
|
||||
### 相关代码参照点(路径)
|
||||
|
||||
- `routers/callback.py`:回复通道选择逻辑(基于 `startswith('#')`)
|
||||
- `games/ai_chat.py`:AI 回复内容生成与返回(直接返回字符串)
|
||||
- `utils/message.py`:`send_markdown()` 与 `send_text()` 的消息结构
|
||||
|
||||
|
||||
### 决策结论与范围(2025-10-30)
|
||||
|
||||
- 分支策略:不创建新分支,继续在当前任务上下文内推进。
|
||||
- 发送策略:`.ai` 产生的回复统一按 Markdown 发送。
|
||||
- 影响范围:仅限 AI 对话功能(`.ai`/`ai_chat`),不扩展到其他指令模块。
|
||||
|
||||
|
||||
# 任务进度(补充)
|
||||
|
||||
## [2025-10-30_??:??:??] 标注 Markdown 渲染问题(记录现状与待确认项)
|
||||
- 已修改:
|
||||
- `.tasks/2025-10-29_3_ai_chat.md`:补充“Markdown 渲染与发送通道”分析与待确认清单(仅问题陈述,无解决方案)。
|
||||
- 更改:
|
||||
- 明确当前仅以标题开头触发 Markdown 发送的启发式导致部分 Markdown 未被渲染。
|
||||
- 原因:
|
||||
- 用户反馈“AI 返回内容支持 Markdown,但当前直接当作文本返回导致无法正确排版”。
|
||||
- 阻碍因素:
|
||||
- 目标平台的 Markdown 渲染细节与统一策略选择待确认。
|
||||
- 状态:
|
||||
- 未确认(等待策略口径与平台渲染规范确认)。
|
||||
|
||||
## [2025-10-30_11:40:31] 执行:AI 回复统一按 Markdown 发送(仅限 AI)
|
||||
- 已修改:
|
||||
- `routers/callback.py`:在 `callback_receive()` 的发送阶段,当 `game_type == 'ai_chat'` 且存在 `response_text` 时,无条件调用 `send_markdown(response_text)`;若发送异常,记录日志并回退到 `send_text(response_text)`;其他指令模块继续沿用 `startswith('#')` 的启发式逻辑。
|
||||
- 更改:
|
||||
- 使 `.ai` 产生的回复在 WPS 端稳定触发 Markdown 渲染,不再依赖以 `#` 开头。
|
||||
- 原因:
|
||||
- 对齐“统一按 Markdown 发送(仅限 AI)”的决策,解决 Markdown 文本被当作纯文本发送导致的排版问题。
|
||||
- 阻碍因素:
|
||||
- 暂无。
|
||||
- 状态:
|
||||
- 成功。
|
||||
@@ -1,531 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-10-30_1_add_casino_games.md
|
||||
创建于:2025-10-30_15:16:56
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/add_casino_games_2025-01-14_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在项目中新增赌场常见游戏,支持多个玩家下注等功能,使用积分系统模拟真实的赌场环境。
|
||||
|
||||
要求:
|
||||
- 指令格式:`.赌场 <游戏类型> <参数>`
|
||||
- 采用模块化设计,便于扩展多种赌场游戏
|
||||
- 支持多人同时下注
|
||||
- 集成现有积分系统
|
||||
- 记录下注和收益数据
|
||||
|
||||
# 项目概览
|
||||
基于WPS协作开放平台的自定义机器人游戏系统。使用FastAPI + SQLite架构,已有完善的积分系统和多款游戏(五子棋、成语接龙等)。需要通过模块化设计添加赌场游戏功能。
|
||||
|
||||
# 分析
|
||||
|
||||
## 现有系统分析
|
||||
1. **积分系统**:已实现 `add_points()` 和 `consume_points()` 方法
|
||||
2. **游戏基类**:`BaseGame` 提供统一的接口
|
||||
3. **路由系统**:通过 `CommandParser` 解析指令,在 `callback.py` 中路由
|
||||
4. **数据库**:SQLite,已有用户表、游戏状态表、统计表
|
||||
|
||||
## 需要新增的内容
|
||||
1. **数据库表**:
|
||||
- `casino_bets`:记录所有下注
|
||||
- `casino_results`:记录游戏结果和结算
|
||||
- `casino_games`:记录游戏房间(可选)
|
||||
|
||||
2. **游戏模块**:
|
||||
- `games/casino.py`:主赌场模块
|
||||
- 第一期支持:大小游戏
|
||||
- 第二期计划:轮盘、二十一点等
|
||||
|
||||
3. **指令映射**:
|
||||
- `.赌场` -> casino 游戏类型
|
||||
- 子指令:`轮盘`、`大小`、`21点` 等
|
||||
|
||||
## 设计要点
|
||||
1. **模块化设计**:每种赌场游戏作为独立类
|
||||
2. **下注流程**:创建房间 -> 玩家下注 -> 结算 -> 分发奖励
|
||||
3. **安全性**:下注前检查积分,结算时原子性操作
|
||||
4. **多玩家支持**:以 chat_id 为单位创建游戏房间
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 指令设计
|
||||
采用 `.赌场 <游戏类型> <操作> <参数>` 的模块化结构:
|
||||
|
||||
### 大小游戏
|
||||
- **庄家开启游戏**:`.赌场 大小 open <最小下注> <最大下注> <赔率>`
|
||||
- 示例:`.赌场 大小 open 10 100 2.0` (最小10分,最大100分,赔率2.0倍)
|
||||
- **玩家下注**:`.赌场 大小 bet <大小/小> <下注金额>`
|
||||
- 示例:`.赌场 大小 bet 大 50` (下注50分压大)
|
||||
- 示例:`.赌场 大小 bet 小 30` (下注30分压小)
|
||||
- **查看状态**:`.赌场 大小 status`
|
||||
- **庄家结算**:`.赌场 大小 settle <结果>`
|
||||
- 示例:`.赌场 大小 settle 大` (开大)
|
||||
- 示例:`.赌场 大小 settle 小` (开小)
|
||||
|
||||
### 轮盘游戏(二期实现)
|
||||
- 暂不实现,等大小游戏完善后再扩展
|
||||
|
||||
### 21点游戏(二期实现)
|
||||
- 暂不实现,等大小游戏完善后再扩展
|
||||
|
||||
## 游戏流程
|
||||
1. 庄家开启游戏(指定下注限额和赔率参数)
|
||||
2. 玩家下注(可多人同时参与)
|
||||
3. 庄家确认结算(手动触发结果)
|
||||
4. 系统自动分发奖励
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 新增表:casino_bets(下注记录表)
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS casino_bets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
game_type TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
bet_type TEXT NOT NULL, -- '大' 或 '小'
|
||||
amount INTEGER NOT NULL,
|
||||
multiplier REAL NOT NULL, -- 赔率
|
||||
status TEXT DEFAULT 'pending', -- pending/settled/cancelled
|
||||
result TEXT, -- 游戏结果
|
||||
win_amount INTEGER, -- 赢得金额
|
||||
created_at INTEGER NOT NULL,
|
||||
settled_at INTEGER,
|
||||
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
||||
)
|
||||
```
|
||||
|
||||
### 新增表:casino_sessions(游戏会话表)
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS casino_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
game_type TEXT NOT NULL,
|
||||
banker_id INTEGER NOT NULL, -- 庄家ID
|
||||
min_bet INTEGER NOT NULL,
|
||||
max_bet INTEGER NOT NULL,
|
||||
multiplier REAL NOT NULL,
|
||||
house_fee REAL DEFAULT 0.05, -- 抽水率,默认5%
|
||||
status TEXT DEFAULT 'open', -- open/settling/closed
|
||||
created_at INTEGER NOT NULL,
|
||||
settled_at INTEGER,
|
||||
UNIQUE(chat_id, game_type, status)
|
||||
)
|
||||
```
|
||||
|
||||
### 索引
|
||||
- `casino_bets(chat_id, game_type, status)` - 快速查询待结算下注
|
||||
- `casino_sessions(chat_id, game_type, status)` - 快速查询活跃游戏
|
||||
|
||||
## 方案对比
|
||||
|
||||
### 方案A:纯数据库表方案(推荐)
|
||||
**优点**:
|
||||
- 数据结构清晰,便于查询统计
|
||||
- 支持历史记录追踪
|
||||
- 并发安全,利用数据库事务
|
||||
- 易于扩展复杂查询
|
||||
|
||||
**缺点**:
|
||||
- 需要维护额外的表结构
|
||||
- 稍微复杂一些
|
||||
|
||||
**决策**:采用此方案
|
||||
|
||||
### 方案B:game_states + JSON方案
|
||||
**优点**:
|
||||
- 复用现有系统
|
||||
- 实现简单
|
||||
|
||||
**缺点**:
|
||||
- 难以进行复杂统计查询
|
||||
- JSON解析性能较差
|
||||
- 数据格式不够规范化
|
||||
|
||||
## 核心实现细节
|
||||
|
||||
### 1. 游戏流程控制
|
||||
- **开启游戏**:检查是否已有活跃游戏,同一chat_id只能有一个进行中的游戏
|
||||
- **下注限制**:检查session状态、下注金额范围、玩家积分
|
||||
- **结算控制**:只有庄家可以结算,结算后自动关闭session
|
||||
|
||||
### 2. 下注流程
|
||||
1. 检查是否有活跃的session
|
||||
2. 检查下注金额是否符合min/max限制
|
||||
3. 检查用户积分是否充足
|
||||
4. 扣除下注金额(consume_points)
|
||||
5. 记录下注到casino_bets表
|
||||
|
||||
### 3. 结算流程
|
||||
1. 验证是否为庄家操作
|
||||
2. 查询所有pending状态的下注
|
||||
3. 计算每个玩家的输赢
|
||||
4. 使用数据库事务确保原子性:
|
||||
- 更新bets状态
|
||||
- 发放/扣除积分
|
||||
- 更新session状态
|
||||
5. 返回结算报告
|
||||
|
||||
### 4. 抽水机制
|
||||
- **抽水率**:5%(可配置,存储在session.house_fee中)
|
||||
- **抽水时机**:从玩家的赢得金额中扣除
|
||||
- **抽水归属**:归系统所有(不返还给庄家)
|
||||
- **计算方式**:
|
||||
- 玩家赢得 = 下注金额 × 赔率
|
||||
- 实际发放 = 赢得金额 × (1 - 抽水率)
|
||||
- 抽水金额 = 赢得金额 × 抽水率
|
||||
|
||||
### 5. 错误处理
|
||||
- 下注时积分不足:给出明确提示
|
||||
- 重复下注:允许(可下多注)
|
||||
- 非法下注金额:给出范围提示
|
||||
- 非庄家尝试结算:拒绝
|
||||
|
||||
## 安全性
|
||||
- 下注前检查积分
|
||||
- 结算时使用数据库事务保证原子性
|
||||
- 抽水机制保护庄家(虽然抽水归系统)
|
||||
- 验证庄家身份
|
||||
- 防止重复结算
|
||||
|
||||
# 详细实施计划
|
||||
|
||||
## 文件1: core/database.py
|
||||
|
||||
### 修改函数: init_tables()
|
||||
在现有表创建之后(约第130行),添加赌场相关表的创建:
|
||||
|
||||
位置:在 `user_points` 表创建之后(约第130行)添加
|
||||
|
||||
```python
|
||||
# 赌场下注记录表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS casino_bets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
game_type TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
bet_type TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
multiplier REAL NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
result TEXT,
|
||||
win_amount INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
settled_at INTEGER,
|
||||
FOREIGN KEY (user_id) REFERENCES users (user_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# 赌场游戏会话表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS casino_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
game_type TEXT NOT NULL,
|
||||
banker_id INTEGER NOT NULL,
|
||||
min_bet INTEGER NOT NULL,
|
||||
max_bet INTEGER NOT NULL,
|
||||
multiplier REAL NOT NULL,
|
||||
house_fee REAL DEFAULT 0.05,
|
||||
status TEXT DEFAULT 'open',
|
||||
created_at INTEGER NOT NULL,
|
||||
settled_at INTEGER,
|
||||
UNIQUE(chat_id, game_type, status)
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建索引
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_casino_bets
|
||||
ON casino_bets(chat_id, game_type, status)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_casino_sessions
|
||||
ON casino_sessions(chat_id, game_type, status)
|
||||
""")
|
||||
```
|
||||
|
||||
### 新增函数: create_casino_session()
|
||||
函数签名:
|
||||
```python
|
||||
def create_casino_session(self, chat_id: int, game_type: str, banker_id: int,
|
||||
min_bet: int, max_bet: int, multiplier: float,
|
||||
house_fee: float = 0.05) -> int:
|
||||
```
|
||||
|
||||
功能:创建新的赌场游戏会话,返回session_id
|
||||
|
||||
### 新增函数: get_active_casino_session()
|
||||
函数签名:
|
||||
```python
|
||||
def get_active_casino_session(self, chat_id: int, game_type: str) -> Optional[Dict]:
|
||||
```
|
||||
|
||||
功能:获取活跃的游戏会话
|
||||
|
||||
### 新增函数: create_casino_bet()
|
||||
函数签名:
|
||||
```python
|
||||
def create_casino_bet(self, chat_id: int, game_type: str, user_id: int,
|
||||
bet_type: str, amount: int, multiplier: float) -> int:
|
||||
```
|
||||
|
||||
功能:创建下注记录,返回bet_id
|
||||
|
||||
### 新增函数: get_pending_bets()
|
||||
函数签名:
|
||||
```python
|
||||
def get_pending_bets(self, chat_id: int, game_type: str) -> List[Dict]:
|
||||
```
|
||||
|
||||
功能:获取待结算的下注列表
|
||||
|
||||
### 新增函数: settle_casino_bets()
|
||||
函数签名:
|
||||
```python
|
||||
def settle_casino_bets(self, chat_id: int, game_type: str, result: str,
|
||||
banker_id: int) -> Dict:
|
||||
```
|
||||
|
||||
功能:结算所有下注,返回结算详情字典(winners, losers, total_win等)
|
||||
|
||||
### 新增函数: close_casino_session()
|
||||
函数签名:
|
||||
```python
|
||||
def close_casino_session(self, chat_id: int, game_type: str):
|
||||
```
|
||||
|
||||
功能:关闭游戏会话
|
||||
|
||||
## 文件2: games/casino.py(新建)
|
||||
|
||||
### 类: CasinoGame
|
||||
继承自 `BaseGame`
|
||||
|
||||
### 方法: __init__()
|
||||
初始化数据库连接
|
||||
|
||||
### 方法: async handle(command, chat_id, user_id) -> str
|
||||
主处理函数,解析指令并调用相应的处理方法
|
||||
|
||||
解析逻辑:
|
||||
- 提取命令参数,格式:`.赌场 <游戏类型> <操作> <参数>`
|
||||
- 识别游戏类型(第一期只支持"大小")
|
||||
- 分发到相应的处理方法
|
||||
|
||||
### 方法: async _handle_bigsmall(command, args, chat_id, user_id) -> str
|
||||
处理大小游戏的各种操作
|
||||
|
||||
支持的操作:
|
||||
- open: 开启游戏
|
||||
- bet: 下注
|
||||
- status: 查看状态
|
||||
- settle: 结算
|
||||
|
||||
### 方法: async _open_bigsmall(args, chat_id, user_id) -> str
|
||||
庄家开启大小游戏
|
||||
|
||||
参数解析:`<最小下注> <最大下注> <赔率>`
|
||||
参数验证和限制
|
||||
|
||||
### 方法: async _bet_bigsmall(args, chat_id, user_id) -> str
|
||||
玩家下注
|
||||
|
||||
参数解析:`<大小/小> <下注金额>`
|
||||
检查session、金额范围、用户积分
|
||||
|
||||
### 方法: async _status_bigsmall(chat_id, game_type) -> str
|
||||
查看当前游戏状态
|
||||
|
||||
### 方法: async _settle_bigsmall(args, chat_id, user_id) -> str
|
||||
庄家结算游戏
|
||||
|
||||
参数解析:`<大/小>`
|
||||
验证庄家身份,结算所有下注
|
||||
|
||||
### 方法: get_help() -> str
|
||||
返回帮助信息
|
||||
|
||||
## 文件3: utils/parser.py
|
||||
|
||||
### 修改: COMMAND_MAP
|
||||
添加赌场指令映射:
|
||||
|
||||
```python
|
||||
# 赌场系统
|
||||
'.赌场': 'casino',
|
||||
'.casino': 'casino',
|
||||
```
|
||||
|
||||
## 文件4: routers/callback.py
|
||||
|
||||
### 修改: async handle_command()
|
||||
在AI对话系统之后(约第209行)添加:
|
||||
|
||||
```python
|
||||
# 赌场系统
|
||||
if game_type == 'casino':
|
||||
from games.casino import CasinoGame
|
||||
game = CasinoGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
## 文件5: games/base.py
|
||||
|
||||
### 修改: get_help_message()
|
||||
在积分赠送系统之后添加赌场游戏帮助:
|
||||
|
||||
```python
|
||||
### 🎰 赌场系统
|
||||
- `.赌场 大小 open <最小> <最大> <赔率>` - 庄家开启大小游戏
|
||||
- `.赌场 大小 bet <大/小> <金额>` - 下注
|
||||
- `.赌场 大小 status` - 查看状态
|
||||
- `.赌场 大小 settle <大/小>` - 庄家结算
|
||||
```
|
||||
|
||||
## 实施清单
|
||||
|
||||
1. 修改 `core/database.py` 的 `init_tables()` 方法,添加赌场表创建和索引
|
||||
2. 在 `core/database.py` 中添加 `create_casino_session()` 方法
|
||||
3. 在 `core/database.py` 中添加 `get_active_casino_session()` 方法
|
||||
4. 在 `core/database.py` 中添加 `create_casino_bet()` 方法
|
||||
5. 在 `core/database.py` 中添加 `get_pending_bets()` 方法
|
||||
6. 在 `core/database.py` 中添加 `settle_casino_bets()` 方法
|
||||
7. 在 `core/database.py` 中添加 `close_casino_session()` 方法
|
||||
8. 创建文件 `games/casino.py`,定义 `CasinoGame` 类
|
||||
9. 在 `games/casino.py` 中实现 `__init__()` 方法
|
||||
10. 在 `games/casino.py` 中实现 `async handle()` 方法
|
||||
11. 在 `games/casino.py` 中实现 `async _handle_bigsmall()` 方法
|
||||
12. 在 `games/casino.py` 中实现 `async _open_bigsmall()` 方法
|
||||
13. 在 `games/casino.py` 中实现 `async _bet_bigsmall()` 方法
|
||||
14. 在 `games/casino.py` 中实现 `async _status_bigsmall()` 方法
|
||||
15. 在 `games/casino.py` 中实现 `async _settle_bigsmall()` 方法
|
||||
16. 在 `games/casino.py` 中实现 `get_help()` 方法
|
||||
17. 修改 `utils/parser.py`,在 COMMAND_MAP 中添加赌场指令映射
|
||||
18. 修改 `routers/callback.py`,在 `handle_command()` 中添加赌场路由
|
||||
19. 修改 `games/base.py`,在 `get_help_message()` 中添加赌场帮助信息
|
||||
20. 测试所有功能点,确保无错误
|
||||
|
||||
# 当前执行步骤:"2. 详细技术规划完成,等待进入实现阶段"
|
||||
|
||||
# 任务进度
|
||||
[2025-10-30_15:16:56]
|
||||
- 已修改:创建任务文件 `.tasks/2025-10-30_1_add_casino_games.md`
|
||||
- 更改:创建任务分支 `task/add_casino_games_2025-01-14_1` 和任务文件
|
||||
- 原因:按照RIPER-5协议建立工作基础
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-30_16:30:00](预估时间)
|
||||
- 已修改:完成详细技术规划
|
||||
- 更改:设计数据库表结构、游戏流程、抽水机制等细节
|
||||
- 原因:为实施阶段提供详细技术规范
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-30_16:07:57]
|
||||
- 已修改:core/database.py, games/casino.py, utils/parser.py, routers/callback.py, games/base.py
|
||||
- 更改:完成所有实施步骤1-19
|
||||
- 添加赌场表创建和索引
|
||||
- 实现6个数据库方法(create_casino_session, get_active_casino_session, create_casino_bet, get_pending_bets, settle_casino_bets, close_casino_session)
|
||||
- 创建完整的CasinoGame类,实现大小游戏所有功能
|
||||
- 注册指令映射和路由
|
||||
- 添加帮助信息
|
||||
- 原因:按照详细实施计划完成全部功能开发
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-30_17:20:00](预估时间)
|
||||
- 已修改:games/casino.py
|
||||
- 更改:修改结算逻辑,从庄家指定结果改为系统随机生成
|
||||
- 移除庄家输入种子/结果的参数
|
||||
- 使用random.random()生成随机结果(50%大/50%小)
|
||||
- 更新帮助信息,settle命令不再需要参数
|
||||
- 原因:用户反馈庄家不应该能够操控游戏结果,庄家也是玩家
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-30_17:26:19]
|
||||
- 已修改:games/casino.py, games/base.py
|
||||
- 更改:添加庄家放弃游戏功能
|
||||
- 新增_cancel_bigsmall()方法处理放弃逻辑
|
||||
- 放弃时返还所有玩家下注
|
||||
- 关闭会话并标记下注为cancelled
|
||||
- 添加cancel命令支持(cancel/放弃/关闭)
|
||||
- 更新帮助信息和base.py中的帮助
|
||||
- 原因:用户要求庄家可以放弃本轮游戏
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_11:35:18]
|
||||
- 已修改:core/database.py
|
||||
- 更改:扩展数据库支持轮盘和21点游戏
|
||||
- 添加列存在性检查辅助方法(_column_exists, _add_column_if_not_exists)
|
||||
- 扩展casino_sessions表:添加current_phase和blackjack_multiplier字段(兼容性检查)
|
||||
- 扩展casino_bets表:添加bet_category、bet_number、bet_value、hand_status字段(兼容性检查)
|
||||
- 创建casino_blackjack_hands表:存储21点游戏手牌数据
|
||||
- 修改create_casino_session():支持单场限制检查(get_any_active_casino_session)和新字段
|
||||
- 扩展create_casino_bet():支持轮盘和21点专用字段参数
|
||||
- 添加21点手牌管理方法:create_blackjack_hand、get_blackjack_hand、update_blackjack_hand、get_all_blackjack_hands
|
||||
- 原因:为轮盘和21点游戏提供数据库支持,确保字段分离和向后兼容
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_15:15:08]
|
||||
- 已修改:core/database.py
|
||||
- 更改:修复大小游戏结算时的UNIQUE约束冲突问题
|
||||
- 移除casino_sessions表的UNIQUE(chat_id, game_type, status)约束
|
||||
- 原因:status='closed'时需要允许多条历史记录,UNIQUE约束阻止了结算时更新status
|
||||
- 添加兼容性迁移逻辑:检测旧版本表结构,自动重建表以移除UNIQUE约束
|
||||
- 迁移时复制所有历史数据,处理外键关系(临时禁用/启用外键检查)
|
||||
- 单场限制通过应用层逻辑(get_any_active_casino_session)保证
|
||||
- 原因:用户测试大小游戏结算时遇到"UNIQUE constraint failed"错误
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_15:15:08]
|
||||
- 已修改:core/database.py
|
||||
- 更改:修复21点游戏结算逻辑问题
|
||||
- 修正losers统计逻辑:将条件从`not player_is_busted and player_points != banker_points`改为`player_points != banker_points`
|
||||
- 原因:原条件排除了爆牌玩家,导致爆牌玩家未被统计到losers列表
|
||||
- 修正数据库更新逻辑:明确区分三种情况
|
||||
- 赢家:发放奖励并更新数据库
|
||||
- 平局(player_points == banker_points):已返还下注,更新数据库
|
||||
- 输家(else分支,包括爆牌和点数小于庄家):更新数据库
|
||||
- 改进结果字符串显示:包含玩家和庄家的状态信息(爆牌、黑杰克等)
|
||||
- 例如:"庄家19点 vs 玩家爆牌" 或 "庄家19点 vs 玩家20点(黑杰克)"
|
||||
- 原因:用户测试21点游戏时发现3人游戏中只有1个赢家被结算,1个爆牌玩家和1个平局玩家未被结算
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_17:24:08]
|
||||
- 已修改:games/casino.py
|
||||
- 更改:重构21点游戏指令流程,改为更符合标准的玩法
|
||||
- 修改_open_blackjack:改为`.赌场 21点 open <底注> <黑杰克倍数>`,移除max_bet参数
|
||||
- 新增_join_blackjack:添加`.赌场 21点 join`指令,玩家加入游戏时扣除底注,检查积分是否足够
|
||||
- 修改_bet_blackjack:改为加注功能,仅在playing阶段可用,加注金额必须不低于底注
|
||||
- 修改_deal_blackjack:实现标准发牌顺序(先玩家1张→庄家明牌→玩家第2张→庄家暗牌),庄家隐藏一张暗牌
|
||||
- 修改_status_blackjack:游戏阶段隐藏庄家暗牌,只显示明牌,结算后显示完整手牌
|
||||
- 修改_stand_blackjack:检查所有玩家是否都已完成(停牌或爆牌),如果所有玩家都完成则自动触发结算
|
||||
- 修改_hit_blackjack:如果爆牌后所有玩家都完成,也自动触发结算
|
||||
- 更新_get_blackjack_help:反映新的指令流程和规则
|
||||
- 原因:用户要求新的指令流程:启动(open)→加入(join)→发牌(deal)→操作(hit/stand/bet加注)→自动结算
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_17:24:08]
|
||||
- 已修改:games/casino.py
|
||||
- 更改:修复停牌和要牌功能中的字典键访问错误
|
||||
- 修复_hit_blackjack中自动结算检查:将`player_hand['hand_status']`改为`player_hand['status']`
|
||||
- 修复_stand_blackjack中自动结算检查:将`player_hand['hand_status']`改为`player_hand['status']`
|
||||
- 原因:`get_all_blackjack_hands`返回的字典结构为`{user_id: {'cards': [...], 'status': ...}}`,应使用`status`而不是`hand_status`
|
||||
- 原因:用户测试停牌功能时遇到KeyError: 'hand_status'错误
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 最终审查
|
||||
待完成
|
||||
@@ -1,93 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-10-31_1_change-adventure-time-to-seconds.md
|
||||
创建于:2025-10-31_17:30:37
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:main
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
将冒险系统的计时单位从分钟更改为秒。需要修改所有相关的时间计算、验证逻辑、显示文本和帮助信息。
|
||||
|
||||
## 后续需求
|
||||
支持时间格式输入(如 `1h30m10s`),并在显示时按时分秒格式显示,避免只显示秒数过于冗余。
|
||||
|
||||
# 项目概览
|
||||
修改 `games/adventure.py` 文件,将冒险时间单位从分钟改为秒。
|
||||
|
||||
# 分析
|
||||
当前代码中所有时间相关逻辑都以分钟为单位:
|
||||
- 默认值:1分钟
|
||||
- 参数验证:至少1分钟
|
||||
- 时间计算:`cost_time * 60` 转换为秒
|
||||
- 显示文本:所有提示信息都显示"分钟"
|
||||
- 奖励计算:倍率 × 分钟数
|
||||
- 放弃冒险:按分钟计算已消耗时间
|
||||
|
||||
需要将所有相关逻辑改为秒为单位。
|
||||
|
||||
# 提议的解决方案
|
||||
将所有时间单位从分钟改为秒:
|
||||
1. 修改默认值和参数验证
|
||||
2. 移除时间计算中的 `* 60` 转换
|
||||
3. 更新所有显示文本
|
||||
4. 简化剩余时间显示逻辑(直接显示秒)
|
||||
5. 更新帮助信息
|
||||
|
||||
# 当前执行步骤:"已完成"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## 2025-10-31_17:32:32
|
||||
- 已修改:games/adventure.py
|
||||
- 更改:
|
||||
1. 修改默认值注释:从"1分钟"改为"1秒"
|
||||
2. 修改函数文档:cost_time参数从"(分钟)"改为"(秒)"
|
||||
3. 修改参数验证:错误提示从"至少需要1分钟"改为"至少需要1秒"
|
||||
4. 移除时间转换:将`end_time = start_time + saved_cost_time * 60`改为`end_time = start_time + saved_cost_time`
|
||||
5. 更新显示文本:所有"分钟"改为"秒"(冒险结果、冒险进行中、冒险开始、冒险放弃)
|
||||
6. 简化剩余时间显示:移除分钟/秒的转换逻辑,直接显示秒数
|
||||
7. 修改放弃冒险逻辑:时间计算改为直接使用秒数(elapsed_seconds),移除分钟转换
|
||||
8. 更新帮助信息:所有"分钟"改为"秒"
|
||||
- 原因:将冒险系统计时单位从分钟改为秒,使时间控制更精确
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## 2025-10-31_17:35:06
|
||||
- 已修改:games/adventure.py
|
||||
- 更改:
|
||||
1. 添加 `re` 模块导入,用于正则表达式解析
|
||||
2. 新增 `_parse_time_string` 方法:解析时间格式字符串,支持以下格式:
|
||||
- 纯数字(按秒):`60` -> 60秒
|
||||
- 时分秒组合:`1h30m10s` -> 5410秒
|
||||
- 分钟秒组合:`30m10s` -> 1810秒
|
||||
- 只有小时:`1h` -> 3600秒
|
||||
- 只有分钟:`30m` -> 1800秒
|
||||
- 只有秒:`10s` -> 10秒
|
||||
3. 新增 `_format_time` 方法:将秒数格式化为 "X时X分X秒" 格式,自动省略为0的部分
|
||||
4. 修改 `handle` 方法:使用 `_parse_time_string` 解析时间参数,提供格式错误提示
|
||||
5. 更新所有时间显示位置:
|
||||
- 冒险结果:使用 `_format_time` 格式化消耗时间
|
||||
- 冒险进行中:使用 `_format_time` 格式化剩余时间和总时长
|
||||
- 冒险开始:使用 `_format_time` 格式化持续时间
|
||||
- 冒险放弃:使用 `_format_time` 格式化已计入时间
|
||||
6. 更新帮助信息:添加时间格式说明和示例
|
||||
- 原因:支持更灵活的时间输入格式,提升用户体验;时间显示按时分秒格式,避免冗长的秒数显示
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## 2025-10-31_17:49:24
|
||||
- 已修改:games/adventure.py
|
||||
- 更改:
|
||||
1. 修复预计完成时间显示问题:
|
||||
- 原问题:只显示小时时刻(`%H:%M:%S`),跨天的冒险无法正确显示,且秒数显示不够明确
|
||||
- 第一次尝试:根据冒险时长是否超过24小时判断(不准确)
|
||||
- 最终解决方案:根据完成时间是否跨天来判断
|
||||
- 跨天或跨年:显示完整日期时间 `YYYY-MM-DD HH:MM:SS`(包含年月日和时分秒)
|
||||
- 同一天:显示时间 `HH:MM:SS`(包含时分秒)
|
||||
- 原因:修复跨天冒险无法正确显示完成时间的问题,只要跨天就显示完整日期,确保秒数清晰显示
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 最终审查
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-11-03_1_user-webhook-url.md
|
||||
创建于:2025-11-03_09:38:30
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/user-webhook-url_2025-11-03_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在WPS Bot Game项目中添加用户专属webhook URL功能,允许每个用户注册自己的个人webhook URL作为私聊途径。
|
||||
|
||||
## 核心需求
|
||||
1. 用户可以通过 `.register url <url>` 指令注册个人webhook URL
|
||||
2. 私聊消息发送功能将被封装为API接口,供其他系统调用
|
||||
3. 提供检测用户是否具有个人URL的接口,用于系统运行时确保参与用户都能被私聊
|
||||
4. 服务器启动时使用的webhook URL称为主URL,私聊用的URL称为个人URL
|
||||
|
||||
## 术语定义
|
||||
- **主URL**: 服务器启动时使用的webhook URL,用于群聊消息发送
|
||||
- **个人URL**: 用户注册的专属webhook URL,用于私聊消息发送
|
||||
|
||||
## 功能要求
|
||||
1. **注册功能**: 支持 `.register url <url>` 指令注册/更新个人URL
|
||||
2. **私聊接口**: 封装私聊消息发送功能为API接口(暂不对用户开放命令)
|
||||
3. **检测接口**: 提供单个和批量检测用户是否有个人URL的接口
|
||||
4. **数据库支持**: 在users表中添加webhook_url字段
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/
|
||||
│ ├── database.py # SQLite数据库操作
|
||||
│ ├── middleware.py # 中间件
|
||||
│ └── models.py # 数据模型
|
||||
├── routers/
|
||||
│ ├── callback.py # Callback路由处理
|
||||
│ ├── health.py # 健康检查
|
||||
│ └── private.py # 私聊相关API(新增)
|
||||
├── games/ # 游戏模块
|
||||
│ └── ... # 各种游戏
|
||||
└── utils/
|
||||
├── parser.py # 指令解析
|
||||
└── message.py # 消息发送
|
||||
```
|
||||
|
||||
# 分析
|
||||
|
||||
## 当前状态
|
||||
1. `users` 表已有基础字段:user_id, username, created_at, last_active
|
||||
2. `routers/callback.py` 中已有 `.register` 命令处理名称注册
|
||||
3. `utils/message.py` 中的 `MessageSender` 类使用全局webhook URL发送消息
|
||||
4. 数据库已支持动态添加列(`_add_column_if_not_exists`方法)
|
||||
5. `init_tables()` 方法在表创建后会进行兼容性检查,使用 `_add_column_if_not_exists` 安全添加新列
|
||||
|
||||
## 关键技术点
|
||||
1. **数据库层**:
|
||||
- 在`init_tables()`中使用`_add_column_if_not_exists`添加`webhook_url`字段(TEXT类型,可为NULL)
|
||||
- 确保兼容性:如果表已存在且没有该列,会自动添加
|
||||
- 添加`set_user_webhook_url(user_id, webhook_url)`方法
|
||||
- 添加`get_user_webhook_url(user_id)`方法
|
||||
- 添加`has_webhook_url(user_id)`方法
|
||||
- 添加`check_users_webhook_urls(user_ids)`批量检查方法
|
||||
|
||||
2. **注册命令扩展**:
|
||||
- 修改`handle_register_command`支持`.register url <url>`子命令
|
||||
- 保留原有的`.register <name>`功能
|
||||
- URL验证(基本格式检查)
|
||||
|
||||
3. **私聊消息发送**:
|
||||
- 封装私聊消息发送功能到`utils/message.py`
|
||||
- 创建`send_private_message(user_id, content, msg_type='text')`函数
|
||||
- 如果用户有个人URL则使用个人URL,否则返回错误
|
||||
|
||||
4. **API接口**:
|
||||
- 创建`routers/private.py`路由文件
|
||||
- `POST /api/private/send` - 发送私聊消息
|
||||
- `GET /api/private/check/{user_id}` - 检查单个用户是否有个人URL
|
||||
- `POST /api/private/check-batch` - 批量检查多个用户
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. **数据库扩展**: 在users表添加webhook_url字段,并实现相关CRUD方法
|
||||
2. **注册命令扩展**: 扩展`.register`命令支持`url`子命令
|
||||
3. **私聊功能封装**: 创建私聊消息发送工具函数
|
||||
4. **API接口**: 创建私聊相关的RESTful API接口
|
||||
|
||||
## 设计决策
|
||||
- 个人URL存储在users表中,与用户信息关联
|
||||
- 私聊功能暂不提供用户命令,仅作为API接口供系统调用
|
||||
- URL验证采用基本格式检查(http/https开头)
|
||||
- 批量检查接口支持传入用户ID列表,返回每个用户的URL状态
|
||||
|
||||
# 当前执行步骤:"3. 执行阶段完成"
|
||||
|
||||
实施清单:
|
||||
1. 在core/database.py的init_tables()方法末尾添加webhook_url字段兼容性检查
|
||||
2. 在core/database.py中添加set_user_webhook_url方法
|
||||
3. 在core/database.py中添加get_user_webhook_url方法
|
||||
4. 在core/database.py中添加has_webhook_url方法
|
||||
5. 在core/database.py中添加check_users_webhook_urls方法
|
||||
6. 在core/models.py文件末尾添加PrivateMessageRequest模型
|
||||
7. 在core/models.py中添加CheckBatchRequest模型
|
||||
8. 在core/models.py中添加CheckBatchResponse模型
|
||||
9. 在core/models.py的导入中添加List类型
|
||||
10. 修改routers/callback.py的handle_register_command函数支持url子命令
|
||||
11. 在utils/message.py文件末尾添加send_private_message函数
|
||||
12. 创建新文件routers/private.py,包含所有私聊相关API接口
|
||||
13. 在app.py中导入private路由模块
|
||||
14. 在app.py中注册private路由
|
||||
|
||||
# 详细实施计划
|
||||
|
||||
## 文件1: core/database.py
|
||||
|
||||
### 修改点1: 在init_tables()方法中添加webhook_url字段兼容性检查
|
||||
|
||||
**位置**: 在`init_tables()`方法的末尾,第324行`logger.info("数据库表初始化完成")`之前
|
||||
|
||||
**修改内容**:
|
||||
```python
|
||||
# 兼容性检查:为users表添加webhook_url字段
|
||||
self._add_column_if_not_exists('users', 'webhook_url', 'TEXT')
|
||||
```
|
||||
|
||||
### 修改点2: 添加set_user_webhook_url方法
|
||||
|
||||
**位置**: 在`# ===== 用户相关操作 =====`部分,`update_user_name`方法之后(约第414行之后)
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def set_user_webhook_url(self, user_id: int, webhook_url: str) -> bool:
|
||||
"""设置用户webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
webhook_url: Webhook URL
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 使用try-except包装
|
||||
- 确保用户存在(调用get_or_create_user)
|
||||
- UPDATE users SET webhook_url = ? WHERE user_id = ?
|
||||
- 记录成功/失败日志
|
||||
- 返回True/False,异常时返回False
|
||||
|
||||
### 修改点3: 添加get_user_webhook_url方法
|
||||
|
||||
**位置**: 紧接`set_user_webhook_url`方法之后
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def get_user_webhook_url(self, user_id: int) -> Optional[str]:
|
||||
"""获取用户webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
Webhook URL,如果不存在返回None
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- SELECT webhook_url FROM users WHERE user_id = ?
|
||||
- 如果查询结果为None,返回None
|
||||
- 如果webhook_url为None或空字符串,返回None
|
||||
- 否则返回URL字符串
|
||||
|
||||
### 修改点4: 添加has_webhook_url方法
|
||||
|
||||
**位置**: 紧接`get_user_webhook_url`方法之后
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def has_webhook_url(self, user_id: int) -> bool:
|
||||
"""检查用户是否有个人webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
是否有个人URL
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 调用get_user_webhook_url
|
||||
- 检查返回值是否不为None且不为空字符串
|
||||
|
||||
### 修改点5: 添加check_users_webhook_urls方法(批量检查)
|
||||
|
||||
**位置**: 紧接`has_webhook_url`方法之后
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def check_users_webhook_urls(self, user_ids: List[int]) -> Dict[int, bool]:
|
||||
"""批量检查用户是否有个人webhook URL
|
||||
|
||||
Args:
|
||||
user_ids: 用户ID列表
|
||||
|
||||
Returns:
|
||||
字典 {user_id: has_url}
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 如果user_ids为空,返回空字典
|
||||
- 使用IN子句查询:SELECT user_id, webhook_url FROM users WHERE user_id IN (?)
|
||||
- 构建结果字典:初始化为所有user_id为False
|
||||
- 遍历查询结果,如果webhook_url不为None且不为空字符串,则设为True
|
||||
- 返回结果字典
|
||||
|
||||
## 文件2: routers/callback.py
|
||||
|
||||
### 修改点1: 修改handle_register_command函数支持url子命令
|
||||
|
||||
**位置**: 第226-260行的`handle_register_command`函数
|
||||
|
||||
**修改内容**:
|
||||
- 提取命令和参数后,检查第一个参数是否为"url"
|
||||
- 如果是"url",提取URL参数,验证URL格式(http/https开头),调用`db.set_user_webhook_url`
|
||||
- 如果不是"url",保持原有逻辑(注册名称)
|
||||
- 更新帮助信息,包含两种用法
|
||||
|
||||
**新的函数逻辑**:
|
||||
```python
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
# 检查是否为url子命令
|
||||
parts = args.split(maxsplit=1)
|
||||
if parts and parts[0].lower() == 'url':
|
||||
# 处理URL注册
|
||||
if len(parts) < 2:
|
||||
return "❌ 请提供webhook URL!\n\n正确格式:`.register url <URL>`\n\n示例:\n`.register url https://example.com/webhook?key=xxx`"
|
||||
webhook_url = parts[1].strip()
|
||||
# URL验证
|
||||
if not webhook_url.startswith(('http://', 'https://')):
|
||||
return "❌ URL格式无效!必须以 http:// 或 https:// 开头。"
|
||||
# 设置URL
|
||||
db = get_db()
|
||||
success = db.set_user_webhook_url(user_id, webhook_url)
|
||||
if success:
|
||||
return f"✅ Webhook URL注册成功!\n\n**您的个人URL**:{webhook_url}\n\n私聊消息将发送到此URL。"
|
||||
else:
|
||||
return "❌ 注册失败!请稍后重试。"
|
||||
else:
|
||||
# 原有的名称注册逻辑
|
||||
...
|
||||
```
|
||||
|
||||
## 文件3: utils/message.py
|
||||
|
||||
### 修改点1: 添加send_private_message函数
|
||||
|
||||
**位置**: 在文件末尾,`get_message_sender`函数之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
async def send_private_message(user_id: int, content: str, msg_type: str = 'text') -> bool:
|
||||
"""发送私聊消息到用户个人webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 目标用户ID
|
||||
content: 消息内容
|
||||
msg_type: 消息类型 ('text' 或 'markdown')
|
||||
|
||||
Returns:
|
||||
是否发送成功,如果用户没有个人URL则返回False
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 从数据库获取用户webhook URL
|
||||
- 如果URL不存在,记录日志并返回False
|
||||
- 创建MessageSender实例(使用用户的个人URL)
|
||||
- 根据msg_type调用send_text或send_markdown
|
||||
- 返回发送结果
|
||||
|
||||
## 文件4: core/models.py (新增数据模型)
|
||||
|
||||
### 修改点1: 添加PrivateMessageRequest模型
|
||||
|
||||
**位置**: 文件末尾
|
||||
|
||||
**模型定义**:
|
||||
```python
|
||||
class PrivateMessageRequest(BaseModel):
|
||||
"""私聊消息请求模型"""
|
||||
user_id: int = Field(..., description="目标用户ID")
|
||||
content: str = Field(..., description="消息内容")
|
||||
msg_type: str = Field(default="text", description="消息类型: text 或 markdown")
|
||||
```
|
||||
|
||||
### 修改点2: 添加CheckBatchRequest模型
|
||||
|
||||
**位置**: 紧接PrivateMessageRequest之后
|
||||
|
||||
**模型定义**:
|
||||
```python
|
||||
class CheckBatchRequest(BaseModel):
|
||||
"""批量检查请求模型"""
|
||||
user_ids: List[int] = Field(..., description="用户ID列表")
|
||||
```
|
||||
|
||||
### 修改点3: 添加CheckBatchResponse模型
|
||||
|
||||
**位置**: 紧接CheckBatchRequest之后
|
||||
|
||||
**模型定义**:
|
||||
```python
|
||||
class CheckBatchResponse(BaseModel):
|
||||
"""批量检查响应模型"""
|
||||
results: Dict[int, bool] = Field(..., description="用户ID到是否有URL的映射")
|
||||
```
|
||||
|
||||
**注意**: core/models.py需要添加`from typing import List`导入(如果尚未导入)
|
||||
|
||||
## 文件5: routers/private.py (新建文件)
|
||||
|
||||
### 文件结构:
|
||||
```python
|
||||
"""私聊相关API路由"""
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from core.database import get_db
|
||||
from core.models import PrivateMessageRequest, CheckBatchRequest, CheckBatchResponse
|
||||
from utils.message import send_private_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
```
|
||||
|
||||
### 接口1: POST /api/private/send
|
||||
|
||||
**位置**: router定义之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
@router.post("/private/send")
|
||||
async def send_private(request: PrivateMessageRequest):
|
||||
"""发送私聊消息
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_id": 123456,
|
||||
"content": "消息内容",
|
||||
"msg_type": "text" // 可选,默认为"text"
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 验证msg_type(必须是"text"或"markdown"),否则返回400错误
|
||||
- 调用send_private_message
|
||||
- 如果返回False(用户没有个人URL或发送失败),返回400错误和相应消息
|
||||
- 成功则返回JSONResponse({"success": True, "message": "消息发送成功"})
|
||||
|
||||
### 接口2: GET /api/private/check/{user_id}
|
||||
|
||||
**位置**: send_private之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
@router.get("/private/check/{user_id}")
|
||||
async def check_user_webhook(user_id: int):
|
||||
"""检查用户是否有个人webhook URL"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 调用db.has_webhook_url(user_id)
|
||||
- 返回JSONResponse({"user_id": user_id, "has_webhook_url": bool})
|
||||
|
||||
### 接口3: POST /api/private/check-batch
|
||||
|
||||
**位置**: check_user_webhook之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
@router.post("/private/check-batch")
|
||||
async def check_users_webhook_batch(request: CheckBatchRequest):
|
||||
"""批量检查用户是否有个人webhook URL
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_ids": [123456, 789012, ...]
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 调用db.check_users_webhook_urls(request.user_ids)
|
||||
- 返回CheckBatchResponse(results=...)
|
||||
|
||||
## 文件6: app.py
|
||||
|
||||
### 修改点1: 导入private路由
|
||||
|
||||
**位置**: 第13行,导入语句中
|
||||
|
||||
**修改内容**:
|
||||
```python
|
||||
from routers import callback, health, private
|
||||
```
|
||||
|
||||
### 修改点2: 注册private路由
|
||||
|
||||
**位置**: 第75-76行,路由注册部分
|
||||
|
||||
**修改内容**:
|
||||
```python
|
||||
app.include_router(callback.router, prefix="/api", tags=["callback"])
|
||||
app.include_router(health.router, tags=["health"])
|
||||
app.include_router(private.router, prefix="/api", tags=["private"])
|
||||
```
|
||||
|
||||
# 任务进度
|
||||
|
||||
[2025-11-03_09:45:56]
|
||||
- 已修改:
|
||||
1. core/database.py - 添加webhook_url字段兼容性检查和4个数据库方法
|
||||
2. core/models.py - 添加3个API数据模型和List类型导入
|
||||
3. routers/callback.py - 扩展handle_register_command支持url子命令
|
||||
4. utils/message.py - 添加send_private_message函数
|
||||
5. routers/private.py - 新建文件,包含3个私聊相关API接口
|
||||
6. app.py - 导入并注册private路由
|
||||
|
||||
- 更改:
|
||||
1. 在users表中添加webhook_url字段支持(兼容性检查)
|
||||
2. 实现用户webhook URL的CRUD操作(设置、获取、检查、批量检查)
|
||||
3. 扩展.register命令支持`.register url <url>`子命令
|
||||
4. 封装私聊消息发送功能为独立函数
|
||||
5. 创建私聊相关的RESTful API接口(发送、单个检查、批量检查)
|
||||
6. 注册新的API路由到FastAPI应用
|
||||
|
||||
- 原因:
|
||||
实现用户专属webhook URL注册和私聊消息发送功能,为其他系统提供API接口调用
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-03_后续]
|
||||
- 已修改:
|
||||
1. utils/parser.py - 添加.talk和.私聊指令映射
|
||||
2. routers/callback.py - 添加handle_talk_command函数实现私聊指令
|
||||
|
||||
- 更改:
|
||||
1. 添加.talk <username> <content>指令,允许用户通过用户名发送私聊消息
|
||||
2. 实现用户名和URL验证,确保目标用户已注册名称和个人URL
|
||||
3. 私聊消息发送成功时不向主URL发送提示消息,保持私密性
|
||||
|
||||
- 原因:
|
||||
实现用户可用的私聊功能,作为私聊功能的开始
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功(测试通过)
|
||||
|
||||
# 最终审查
|
||||
|
||||
待审查阶段完成...
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-11-03_2_werewolf-game.md
|
||||
创建于:2025-11-03_12:20:10
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/werewolf_2025-11-03_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在WPS Bot Game项目中添加狼人杀游戏系统,支持6-12人游戏,包含身份分配、私聊功能、技能使用等核心功能。
|
||||
|
||||
## 核心需求
|
||||
1. 支持6-12人狼人杀游戏(配置:2-4狼 1预言家 1女巫 2-6平民)
|
||||
2. 主持人开房:`.狼人杀 open`
|
||||
3. 玩家加入:`.狼人杀 join`(必须注册用户名和个人URL)
|
||||
4. 开始游戏:`.狼人杀 start`,自动分配身份并通过私聊发送
|
||||
5. 私聊功能:`.狼人杀 <玩家代号> <消息>`
|
||||
6. 狼人群聊:`.狼人杀 狼人 <消息>`
|
||||
7. 技能系统:杀、验、救、毒
|
||||
8. 游戏状态查询:`.狼人杀 status`
|
||||
9. 结束游戏:`.狼人杀 end`
|
||||
|
||||
## 游戏规则
|
||||
**人数配置**:
|
||||
- 6人:2狼 1预言家 1女巫 2平民
|
||||
- 8人:2狼 1预言家 1女巫 4平民
|
||||
- 10人:3狼 1预言家 1女巫 5平民
|
||||
- 12人:4狼 1预言家 1女巫 6平民
|
||||
|
||||
**胜利条件**:
|
||||
- 狼人阵营:杀死所有神职和平民
|
||||
- 好人阵营:消灭所有狼人
|
||||
|
||||
**技能**:
|
||||
- 狼人:每晚投票刀人
|
||||
- 预言家:每晚查验一个玩家身份
|
||||
- 女巫:拥有一瓶解药(仅可使用一次)和一瓶毒药(仅可使用一次)
|
||||
- 平民:无特殊技能
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/
|
||||
│ ├── database.py # SQLite数据库操作
|
||||
│ ├── middleware.py # 中间件
|
||||
│ └── models.py # 数据模型
|
||||
├── routers/
|
||||
│ ├── callback.py # Callback路由处理
|
||||
│ ├── health.py # 健康检查
|
||||
│ └── private.py # 私聊相关API
|
||||
├── games/ # 游戏模块
|
||||
│ ├── werewolf.py # 狼人杀游戏(新增)
|
||||
│ └── ... # 其他游戏
|
||||
└── utils/
|
||||
├── parser.py # 指令解析
|
||||
└── message.py # 消息发送
|
||||
```
|
||||
|
||||
# 分析
|
||||
|
||||
## 当前状态
|
||||
1. 已有私聊功能支持,用户可注册个人webhook URL
|
||||
2. 已有游戏架构:BaseGame基类、数据库状态管理
|
||||
3. 已有成语接龙等多人类游戏参考
|
||||
4. 数据库支持game_states表存储游戏状态
|
||||
|
||||
## 关键技术点
|
||||
1. **数据库层**:
|
||||
- 使用game_states表存储游戏状态(user_id=0表示群级别状态)
|
||||
- 通过state_data JSON字段存储玩家列表、身份、阶段等信息
|
||||
|
||||
2. **私聊系统**:
|
||||
- 利用现有的send_private_message函数
|
||||
- 身份信息、技能结果等通过私聊发送
|
||||
- 支持发送者标识显示
|
||||
|
||||
3. **游戏状态管理**:
|
||||
- 游戏状态存储在state_data中
|
||||
- 包含:玩家列表、身份映射、狼人列表、技能使用记录等
|
||||
|
||||
4. **技能系统**:
|
||||
- 狼人刀人:投票机制
|
||||
- 预言家验人:私聊返回结果
|
||||
- 女巫用药:限制使用次数
|
||||
|
||||
5. **指令路由**:
|
||||
- 在parser.py注册.werewolf和.狼人杀指令
|
||||
- 在callback.py中导入并注册游戏处理器
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. **创建狼人杀游戏类**:`games/werewolf.py`,继承BaseGame
|
||||
2. **状态数据结构**:使用JSON存储在state_data中
|
||||
3. **身份分配**:随机分配角色,狼人互相认识
|
||||
4. **私聊通知**:游戏开始时通过私聊发送身份信息
|
||||
5. **技能系统**:支持杀、验、救、毒四种技能
|
||||
6. **指令注册**:添加解析和路由支持
|
||||
|
||||
## 设计决策
|
||||
- 使用user_id=0存储群级别游戏状态(参考成语接龙)
|
||||
- 通过私聊发送敏感信息(身份、技能结果)
|
||||
- 简化实现:主持人手动推进阶段(暂不实现自动流转)
|
||||
- 使用数字代号(1-N)标识玩家
|
||||
- 支持狼人群聊功能
|
||||
|
||||
# 当前执行步骤:"实施完成"
|
||||
|
||||
# 详细实施计划
|
||||
|
||||
## 文件1: games/werewolf.py(新建文件)
|
||||
|
||||
### 主要方法
|
||||
1. **handle()** - 主处理逻辑,指令路由
|
||||
2. **get_help()** - 帮助信息
|
||||
3. **_open_game()** - 主持人开房
|
||||
4. **_join_game()** - 玩家加入
|
||||
5. **_start_game()** - 开始游戏,分配身份
|
||||
6. **_send_identities()** - 私聊发送身份信息
|
||||
7. **_private_chat()** - 玩家私聊
|
||||
8. **_wolf_group_chat()** - 狼人群聊
|
||||
9. **_handle_skill()** - 技能处理
|
||||
10. **_wolf_kill()** - 狼人刀人
|
||||
11. **_seer_check()** - 预言家验人
|
||||
12. **_witch_save()** - 女巫救人
|
||||
13. **_witch_poison()** - 女巫毒人
|
||||
14. **_show_status()** - 显示游戏状态
|
||||
15. **_end_game()** - 结束游戏
|
||||
|
||||
### 数据结构设计
|
||||
```python
|
||||
state_data = {
|
||||
'creator_id': int, # 主持人ID
|
||||
'status': str, # 'open', 'playing', 'finished'
|
||||
'players': [
|
||||
{
|
||||
'user_id': int,
|
||||
'name': str, # 注册的用户名
|
||||
'id': int, # 游戏内代号 1-N
|
||||
'role': str, # 'wolf', 'seer', 'witch', 'civilian'
|
||||
'alive': bool,
|
||||
'id_label': str # "1号玩家"
|
||||
}
|
||||
],
|
||||
'phase': str, # 当前阶段
|
||||
'round': int, # 当前回合数
|
||||
'wolves': [int], # 狼人ID列表
|
||||
'kill_target': int, # 狼人票决目标
|
||||
'seer_result': {}, # 预言家验人结果
|
||||
'witch_save': bool, # 女巫是否救人
|
||||
'witch_poison': int, # 女巫毒杀目标
|
||||
'discussed': False, # 讨论阶段是否完成
|
||||
'wolf_know_each_other': False
|
||||
}
|
||||
```
|
||||
|
||||
## 文件2: utils/parser.py
|
||||
|
||||
### 修改点:添加指令映射
|
||||
在COMMAND_MAP中添加:
|
||||
```python
|
||||
'.werewolf': 'werewolf',
|
||||
'.狼人杀': 'werewolf',
|
||||
```
|
||||
|
||||
## 文件3: routers/callback.py
|
||||
|
||||
### 修改点:添加路由处理
|
||||
在handle_command函数中添加:
|
||||
```python
|
||||
# 狼人杀系统
|
||||
if game_type == 'werewolf':
|
||||
from games.werewolf import WerewolfGame
|
||||
game = WerewolfGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
## 文件4: games/base.py
|
||||
|
||||
### 修改点:添加帮助信息
|
||||
在get_help_message()函数中添加狼人杀帮助说明
|
||||
|
||||
# 任务进度
|
||||
|
||||
[2025-11-03_12:20:10]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 新建狼人杀游戏类,实现所有核心功能
|
||||
2. utils/parser.py - 添加.werewolf和.狼人杀指令映射
|
||||
3. routers/callback.py - 添加狼人杀路由处理
|
||||
4. games/base.py - 添加狼人杀帮助信息
|
||||
|
||||
- 更改:
|
||||
1. 创建完整的狼人杀游戏系统
|
||||
2. 支持开房、加入、开始、私聊、技能使用等所有核心功能
|
||||
3. 实现6-12人游戏配置和角色分配
|
||||
4. 集成私聊系统发送身份信息
|
||||
5. 支持狼人群聊功能
|
||||
6. 添加帮助信息和指令注册
|
||||
|
||||
- 原因:
|
||||
实现完整的狼人杀游戏系统,支持多人游戏、身份隐藏、技能使用等核心功能
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-04_17:41:14]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 改进阶段提示和自动流转功能
|
||||
|
||||
- 更改:
|
||||
1. 添加阶段名称映射系统(phase_configs),定义各阶段的中文名称、行动角色和指令说明
|
||||
2. 实现阶段管理方法:_get_phase_description()、_get_next_phase()、_advance_phase()
|
||||
3. 改进游戏开始提示,明确显示"第一夜 - 狼人行动阶段"和操作指令
|
||||
4. 实现自动阶段流转:狼人刀人后自动进入预言家阶段,预言家验人后自动进入女巫阶段
|
||||
5. 新增女巫跳过功能:支持`.werewolf 跳过`指令,女巫可以选择不行动
|
||||
6. 改进状态显示:_show_status()现在显示中文阶段名称、当前行动角色和操作指令
|
||||
7. 更新身份说明和帮助信息,添加女巫跳过选项说明
|
||||
8. 各技能方法添加阶段验证,确保在正确的阶段使用技能
|
||||
|
||||
- 原因:
|
||||
解决用户反馈的游戏阶段不明显的问题,让玩家清楚知道当前是什么阶段、谁应该行动、下一步是什么阶段
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
# 最终审查
|
||||
|
||||
待审查阶段完成...
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
57
.tasks/2025-11-12_3_plugin-illustrated-guide.md
Normal file
57
.tasks/2025-11-12_3_plugin-illustrated-guide.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 背景
|
||||
文件名:2025-11-12_3_plugin-illustrated-guide.md
|
||||
创建于:2025-11-12_19:34:38
|
||||
创建者:liubai095\asus
|
||||
主分支:main
|
||||
任务分支:未创建
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
为 Plugins 目录下的插件实例类补充 generate_router_illustrated_guide 所需图鉴内容,覆盖指令、物品、配方与指引信息。
|
||||
|
||||
# 项目概览
|
||||
NewWPSBot 插件体系基于 PWF 核心模块提供的插件接口,Plugins 目录包含多套子系统(炼金、背包、花园等),需确保图鉴内容与各系统已有功能保持一致。
|
||||
|
||||
# 分析
|
||||
已确认 `PWF/CoreModules/plugin_interface.py` 新增 `generate_router_illustrated_guide` 接口,默认返回 `None`,需要在各插件实现中补充网页渲染函数。`Plugins/WPSAPI.py` 定义基础 `WPSAPI`/`BasicWPSInterface`,大部分业务插件继承于此。当前插件依赖链条大致为:`WPSAPI` → 核心系统(`WPSBackpackSystem`, `WPSConfigAPI`, `WPSStoreSystem`, `WPSFortuneSystem`, `WPSAlchemyGame` 等)→ 领域子系统(菜园、战斗、水晶等)→ 命令子插件(如菜园种植/收获/偷取、战斗冒险/装备/营地等)。各子系统内部大量复用 `garden_models`, `combat_models`, `crystal_models` 等数据定义,需要在图鉴构建时引用既有配置与枚举确保口径一致。`WPSStoreSystem`, `WPSCombatSystem`, `WPSGardenSystem`, `WPSAlchemyGame` 均在 `wake_up` 阶段注册指令与物品/配方,需综合其注册内容和服务方法生成指令说明、物品清单、流程提示。后续还需梳理 `.tasks` 历史记录以了解已实现的攻略需求,避免遗漏新增指令。
|
||||
|
||||
# 提议的解决方案
|
||||
待进入 INNOVATE 模式后补充。
|
||||
|
||||
# 当前执行步骤:"1. 研究插件架构"
|
||||
|
||||
# 任务进度
|
||||
2025-11-12_20:33:53
|
||||
- 已修改:Plugins/WPSAPI.py Plugins/WPSBackpackSystem.py Plugins/WPSConfigSystem.py Plugins/WPSStoreSystem.py Plugins/WPSFortuneSystem.py Plugins/WPSAlchemyGame.py Plugins/WPSGardenSystem/garden_plugin_base.py Plugins/WPSGardenSystem/garden_plugin_plant.py Plugins/WPSGardenSystem/garden_plugin_harvest.py Plugins/WPSGardenSystem/garden_plugin_remove.py Plugins/WPSGardenSystem/garden_plugin_view.py Plugins/WPSGardenSystem/garden_plugin_steal.py Plugins/WPSCrystalSystem/crystal_plugin_base.py Plugins/WPSCombatSystem/combat_plugin_base.py Plugins/WPSCombatSystem/combat_plugin_adventure.py Plugins/WPSCombatSystem/combat_plugin_battle.py Plugins/WPSCombatSystem/combat_plugin_camp.py Plugins/WPSCombatSystem/combat_plugin_equipment.py Plugins/WPSCombatSystem/combat_plugin_heal.py Plugins/WPSCombatSystem/combat_plugin_status.py
|
||||
- 更改:在 WPSAPI 基类构建图鉴数据模型与 Markdown 渲染模板,并为核心系统与各子插件补充指令、物品与流程说明
|
||||
- 原因:实现 generate_router_illustrated_guide 所需的图鉴内容结构
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
2025-11-12_21:45:41
|
||||
- 已修改:Plugins/WPSAPI.py
|
||||
- 更改:引入 Apple Store 风格的 HTML 渲染模板,返回 HTMLResponse 并扩展图鉴条目字段以支持图标、徽章等视觉元素
|
||||
- 原因:提升图鉴网页展示效果并支持富样式渲染
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
2025-11-12_22:21:28
|
||||
- 已修改:Plugins/WPSBackpackSystem.py Plugins/WPSGardenSystem/garden_plugin_base.py Plugins/WPSGardenSystem/garden_plugin_plant.py Plugins/WPSGardenSystem/garden_plugin_harvest.py Plugins/WPSGardenSystem/garden_plugin_remove.py Plugins/WPSGardenSystem/garden_plugin_view.py Plugins/WPSGardenSystem/garden_plugin_steal.py Plugins/WPSAlchemyGame.py Plugins/WPSCombatSystem/combat_plugin_base.py Plugins/WPSCombatSystem/combat_plugin_adventure.py Plugins/WPSCombatSystem/combat_plugin_battle.py Plugins/WPSCombatSystem/combat_plugin_equipment.py Plugins/WPSCombatSystem/combat_plugin_camp.py Plugins/WPSCombatSystem/combat_plugin_heal.py Plugins/WPSCombatSystem/combat_plugin_status.py
|
||||
- 更改:为背包、菜园、炼金与战斗系统图鉴补充详细条目,枚举各物品、配方与指令步骤,并在页面中渲染完整细节
|
||||
- 原因:解决仅展示概览的问题,让图鉴列出全部物品和操作细节
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
2025-11-12_22:37:49
|
||||
- 已修改:Plugins/WPSAPI.py
|
||||
- 更改:补充类关系跳转功能,图鉴页面可展示父类链与子类列表并跳转至对应插件页面
|
||||
- 原因:便于从页面直接浏览继承体系
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
2025-11-12_22:43:53
|
||||
- 已修改:Plugins/WPSAPI.py
|
||||
- 更改:适配插件路由改成 `/ClassName`,更新图鉴类关系链接为 `/api/<类名>`
|
||||
- 原因:保持跳转链接与最新路由结构一致
|
||||
- 阻碍因素:无
|
||||
- 状态:未确认
|
||||
|
||||
# 最终审查
|
||||
|
||||
|
||||
2
PWF
2
PWF
Submodule PWF updated: ca3cf114e3...9cb259f2c7
@@ -5,6 +5,9 @@ from PWF.Convention.Runtime.Architecture import Architecture
|
||||
from PWF.Convention.Runtime.GlobalConfig import ProjectConfig
|
||||
from PWF.Convention.Runtime.Web import ToolURL
|
||||
from PWF.Convention.Runtime.String import LimitStringLength
|
||||
from fastapi.responses import HTMLResponse
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, TypedDict, override, Union
|
||||
import httpx
|
||||
import re
|
||||
|
||||
@@ -13,6 +16,613 @@ MAIN_WEBHOOK_URL = logger.FindItem("main_webhook_url", "")
|
||||
logger.SaveProperties()
|
||||
|
||||
|
||||
class GuideEntry(TypedDict, total=False):
|
||||
"""单条图鉴信息。"""
|
||||
|
||||
title: str
|
||||
identifier: str
|
||||
description: str
|
||||
category: str
|
||||
metadata: Dict[str, str]
|
||||
icon: str
|
||||
badge: str
|
||||
links: Sequence[Dict[str, str]]
|
||||
tags: Sequence[str]
|
||||
details: Sequence[Union[str, Dict[str, Any]]]
|
||||
group: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GuideSection:
|
||||
"""图鉴章节。"""
|
||||
|
||||
title: str
|
||||
entries: Sequence[GuideEntry] = field(default_factory=tuple)
|
||||
description: str = ""
|
||||
layout: str = "grid"
|
||||
section_id: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GuidePage:
|
||||
"""完整图鉴页面。"""
|
||||
|
||||
title: str
|
||||
sections: Sequence[GuideSection] = field(default_factory=tuple)
|
||||
subtitle: str = ""
|
||||
metadata: Dict[str, str] = field(default_factory=dict)
|
||||
related_links: Dict[str, Sequence[Dict[str, str]]] = field(default_factory=dict)
|
||||
|
||||
|
||||
def render_markdown_page(page: GuidePage) -> str:
|
||||
"""保留 Markdown 渲染(备用)。"""
|
||||
|
||||
def _render_section(section: GuideSection) -> str:
|
||||
lines: List[str] = [f"## {section.title}"]
|
||||
if section.description:
|
||||
lines.append(section.description)
|
||||
if not section.entries:
|
||||
lines.append("> 暂无内容。")
|
||||
return "\n".join(lines)
|
||||
for entry in section.entries:
|
||||
title = entry.get("title", "未命名")
|
||||
identifier = entry.get("identifier")
|
||||
desc = entry.get("description", "")
|
||||
category = entry.get("category")
|
||||
metadata = entry.get("metadata", {})
|
||||
bullet = f"- **{title}**"
|
||||
if identifier:
|
||||
bullet += f"|`{identifier}`"
|
||||
if category:
|
||||
bullet += f"|{category}"
|
||||
lines.append(bullet)
|
||||
if desc:
|
||||
lines.append(f" - {desc}")
|
||||
for meta_key, meta_value in metadata.items():
|
||||
lines.append(f" - {meta_key}:{meta_value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
lines: List[str] = [f"# {page.title}"]
|
||||
if page.subtitle:
|
||||
lines.append(page.subtitle)
|
||||
if page.metadata:
|
||||
lines.append("")
|
||||
for key, value in page.metadata.items():
|
||||
lines.append(f"- {key}:{value}")
|
||||
for section in page.sections:
|
||||
lines.append("")
|
||||
lines.append(_render_section(section))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_html_page(page: GuidePage) -> str:
|
||||
"""渲染 Apple Store 风格的 HTML 页面。"""
|
||||
|
||||
def escape(text: Optional[str]) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
return (
|
||||
text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace('"', """)
|
||||
)
|
||||
|
||||
def render_metadata(metadata: Dict[str, str]) -> str:
|
||||
if not metadata:
|
||||
return ""
|
||||
cards = []
|
||||
for key, value in metadata.items():
|
||||
cards.append(
|
||||
f"""
|
||||
<div class="meta-card">
|
||||
<div class="meta-key">{escape(key)}</div>
|
||||
<div class="meta-value">{escape(value)}</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
return f'<section class="meta-grid">{"".join(cards)}</section>'
|
||||
|
||||
def render_links(links: Optional[Sequence[Dict[str, str]]]) -> str:
|
||||
if not links:
|
||||
return ""
|
||||
items = []
|
||||
for link in links:
|
||||
href = escape(link.get("href", "#"))
|
||||
label = escape(link.get("label", "前往"))
|
||||
items.append(f'<a class="entry-link" href="{href}" target="_blank">{label}</a>')
|
||||
return "".join(items)
|
||||
|
||||
def render_tags(tags: Optional[Sequence[str]]) -> str:
|
||||
if not tags:
|
||||
return ""
|
||||
chips = "".join(f'<span class="entry-tag">{escape(tag)}</span>' for tag in tags)
|
||||
return f'<div class="entry-tags-extra">{chips}</div>'
|
||||
|
||||
def render_details(details: Optional[Sequence[Union[str, Dict[str, Any]]]]) -> str:
|
||||
if not details:
|
||||
return ""
|
||||
blocks: List[str] = []
|
||||
for detail in details:
|
||||
if isinstance(detail, str):
|
||||
blocks.append(f'<p class="entry-detail-paragraph">{escape(detail)}</p>')
|
||||
elif isinstance(detail, dict):
|
||||
kind = detail.get("type")
|
||||
if kind == "list":
|
||||
items = "".join(
|
||||
f'<li>{escape(str(item))}</li>' for item in detail.get("items", [])
|
||||
)
|
||||
blocks.append(f'<ul class="entry-detail-list">{items}</ul>')
|
||||
elif kind == "steps":
|
||||
items = "".join(
|
||||
f'<li><span class="step-index">{idx+1}</span><span>{escape(str(item))}</span></li>'
|
||||
for idx, item in enumerate(detail.get("items", []))
|
||||
)
|
||||
blocks.append(f'<ol class="entry-detail-steps">{items}</ol>')
|
||||
elif kind == "table":
|
||||
rows = []
|
||||
for row in detail.get("rows", []):
|
||||
cols = "".join(f"<td>{escape(str(col))}</td>" for col in row)
|
||||
rows.append(f"<tr>{cols}</tr>")
|
||||
head = ""
|
||||
headers = detail.get("headers")
|
||||
if headers:
|
||||
head = "".join(f"<th>{escape(str(col))}</th>" for col in headers)
|
||||
head = f"<thead><tr>{head}</tr></thead>"
|
||||
blocks.append(
|
||||
f'<table class="entry-detail-table">{head}<tbody>{"".join(rows)}</tbody></table>'
|
||||
)
|
||||
if not blocks:
|
||||
return ""
|
||||
return f'<div class="entry-details">{"".join(blocks)}</div>'
|
||||
|
||||
def render_entry(entry: GuideEntry) -> str:
|
||||
icon = escape(entry.get("icon"))
|
||||
badge = escape(entry.get("badge"))
|
||||
title = escape(entry.get("title"))
|
||||
identifier = escape(entry.get("identifier"))
|
||||
description = escape(entry.get("description"))
|
||||
category = escape(entry.get("category"))
|
||||
metadata_items = []
|
||||
for meta_key, meta_value in entry.get("metadata", {}).items():
|
||||
metadata_items.append(
|
||||
f'<li><span>{escape(meta_key)}</span><span>{escape(str(meta_value))}</span></li>'
|
||||
)
|
||||
metadata_html = ""
|
||||
if metadata_items:
|
||||
metadata_html = f'<ul class="entry-meta">{"".join(metadata_items)}</ul>'
|
||||
identifier_html = f'<code class="entry-id">{identifier}</code>' if identifier else ""
|
||||
category_html = f'<span class="entry-category">{category}</span>' if category else ""
|
||||
badge_html = f'<span class="entry-badge">{badge}</span>' if badge else ""
|
||||
icon_html = f'<div class="entry-icon">{icon}</div>' if icon else ""
|
||||
links_html = render_links(entry.get("links"))
|
||||
tags_html = render_tags(entry.get("tags"))
|
||||
details_html = render_details(entry.get("details"))
|
||||
group = escape(entry.get("group"))
|
||||
group_attr = f' data-group="{group}"' if group else ""
|
||||
return f"""
|
||||
<article class="entry-card"{group_attr}>
|
||||
{icon_html}
|
||||
<div class="entry-content">
|
||||
<header>
|
||||
<h3>{title}{badge_html}</h3>
|
||||
<div class="entry-tags">{identifier_html}{category_html}</div>
|
||||
</header>
|
||||
<p class="entry-desc">{description}</p>
|
||||
{metadata_html}
|
||||
{tags_html}
|
||||
{details_html}
|
||||
{links_html}
|
||||
</div>
|
||||
</article>
|
||||
"""
|
||||
|
||||
def render_section(section: GuideSection) -> str:
|
||||
layout_class = "entries-grid" if section.layout == "grid" else "entries-list"
|
||||
section_attr = f' id="{escape(section.section_id)}"' if section.section_id else ""
|
||||
cards = "".join(render_entry(entry) for entry in section.entries)
|
||||
description_html = (
|
||||
f'<p class="section-desc">{escape(section.description)}</p>'
|
||||
if section.description
|
||||
else ""
|
||||
)
|
||||
if not cards:
|
||||
cards = '<div class="empty-placeholder">暂无内容</div>'
|
||||
return f"""
|
||||
<section class="guide-section"{section_attr}>
|
||||
<div class="section-header">
|
||||
<h2>{escape(section.title)}</h2>
|
||||
{description_html}
|
||||
</div>
|
||||
<div class="{layout_class}">
|
||||
{cards}
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
|
||||
def render_related(related: Dict[str, Sequence[Dict[str, str]]]) -> str:
|
||||
if not related:
|
||||
return ""
|
||||
blocks: List[str] = []
|
||||
for label, links in related.items():
|
||||
if not links:
|
||||
continue
|
||||
items = "".join(
|
||||
f'<a class="related-link" href="{escape(link.get("href", "#"))}">{escape(link.get("label", ""))}</a>'
|
||||
for link in links
|
||||
)
|
||||
blocks.append(
|
||||
f"""
|
||||
<div class="related-block">
|
||||
<div class="related-label">{escape(label)}</div>
|
||||
<div class="related-items">{items}</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
if not blocks:
|
||||
return ""
|
||||
return f'<section class="related-section">{"".join(blocks)}</section>'
|
||||
|
||||
sections_html = "".join(render_section(section) for section in page.sections)
|
||||
metadata_html = render_metadata(page.metadata)
|
||||
related_html = render_related(page.related_links)
|
||||
subtitle_html = f'<p class="hero-subtitle">{escape(page.subtitle)}</p>' if page.subtitle else ""
|
||||
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{escape(page.title)}</title>
|
||||
<style>
|
||||
:root {{
|
||||
color-scheme: dark;
|
||||
--bg-primary: radial-gradient(120% 120% at 10% 10%, #222 0%, #050505 100%);
|
||||
--bg-card: rgba(255, 255, 255, 0.06);
|
||||
--bg-card-hover: rgba(255, 255, 255, 0.12);
|
||||
--border-soft: rgba(255, 255, 255, 0.1);
|
||||
--text-main: #f5f5f7;
|
||||
--text-sub: rgba(245, 245, 247, 0.64);
|
||||
--accent: linear-gradient(135deg, #4b7bec, #34e7e4);
|
||||
--accent-strong: linear-gradient(135deg, #ff9f1a, #ff3f34);
|
||||
}}
|
||||
* {{
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
margin: 0;
|
||||
font-family: "SF Pro Display", "SF Pro SC", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
padding: 0 0 64px;
|
||||
}}
|
||||
a {{
|
||||
color: #9fc9ff;
|
||||
text-decoration: none;
|
||||
}}
|
||||
a:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
header.hero {{
|
||||
padding: 80px 24px 40px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}}
|
||||
header.hero::after {{
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 50% -20%, rgba(255, 255, 255, 0.18), transparent 55%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}}
|
||||
.hero-title {{
|
||||
font-size: clamp(32px, 5vw, 48px);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
background: var(--accent);
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
letter-spacing: 0.8px;
|
||||
}}
|
||||
.hero-subtitle {{
|
||||
margin: 16px auto 0;
|
||||
max-width: 640px;
|
||||
font-size: 18px;
|
||||
color: var(--text-sub);
|
||||
line-height: 1.6;
|
||||
}}
|
||||
main {{
|
||||
width: min(1100px, 92vw);
|
||||
margin: 0 auto;
|
||||
}}
|
||||
.meta-grid {{
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
margin: 0 auto 48px;
|
||||
padding: 0 8px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}}
|
||||
.related-section {{
|
||||
margin: 0 auto 48px;
|
||||
padding: 0 8px;
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}}
|
||||
.related-block {{
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}}
|
||||
.related-label {{
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-sub);
|
||||
}}
|
||||
.related-items {{
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}}
|
||||
.related-link {{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.4px;
|
||||
transition: background 0.2s ease;
|
||||
}}
|
||||
.related-link:hover {{
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
text-decoration: none;
|
||||
}}
|
||||
.meta-card {{
|
||||
border-radius: 18px;
|
||||
padding: 22px;
|
||||
border: 1px solid var(--border-soft);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
|
||||
}}
|
||||
.meta-key {{
|
||||
font-size: 13px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-sub);
|
||||
}}
|
||||
.meta-value {{
|
||||
margin-top: 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.guide-section {{
|
||||
margin: 0 auto 56px;
|
||||
padding: 0 8px;
|
||||
}}
|
||||
.section-header {{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 28px;
|
||||
}}
|
||||
.section-header h2 {{
|
||||
margin: 0;
|
||||
font-size: clamp(26px, 3vw, 32px);
|
||||
letter-spacing: 0.5px;
|
||||
}}
|
||||
.section-desc {{
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: var(--text-sub);
|
||||
max-width: 640px;
|
||||
}}
|
||||
.entries-grid {{
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
}}
|
||||
.entries-list {{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}}
|
||||
.entry-card {{
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
border: 1px solid transparent;
|
||||
background: var(--bg-card);
|
||||
transition: border 0.2s ease, background 0.2s ease, transform 0.2s ease;
|
||||
}}
|
||||
.entry-card:hover {{
|
||||
background: var(--bg-card-hover);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
transform: translateY(-4px);
|
||||
}}
|
||||
.entry-icon {{
|
||||
font-size: 36px;
|
||||
}}
|
||||
.entry-content {{
|
||||
flex: 1;
|
||||
}}
|
||||
.entry-content header {{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
.entry-content h3 {{
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}}
|
||||
.entry-badge {{
|
||||
display: inline-flex;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
background: var(--accent-strong);
|
||||
}}
|
||||
.entry-tags {{
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}}
|
||||
.entry-tags-extra {{
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}}
|
||||
.entry-id {{
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}}
|
||||
.entry-category {{
|
||||
font-size: 12px;
|
||||
color: var(--text-sub);
|
||||
}}
|
||||
.entry-desc {{
|
||||
margin: 0 0 12px;
|
||||
color: var(--text-sub);
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.entry-meta {{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text-sub);
|
||||
}}
|
||||
.entry-meta li {{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}}
|
||||
.entry-link {{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}}
|
||||
.entry-tag {{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.4px;
|
||||
}}
|
||||
.entry-details {{
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}}
|
||||
.entry-detail-paragraph {{
|
||||
margin: 0;
|
||||
color: var(--text-sub);
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.entry-detail-list, .entry-detail-steps {{
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: var(--text-sub);
|
||||
}}
|
||||
.entry-detail-steps {{
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}}
|
||||
.entry-detail-steps li {{
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr;
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
}}
|
||||
.entry-detail-steps .step-index {{
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
font-size: 12px;
|
||||
}}
|
||||
.entry-detail-table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}}
|
||||
.entry-detail-table th,
|
||||
.entry-detail-table td {{
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
}}
|
||||
.entry-detail-table th {{
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
font-weight: 600;
|
||||
}}
|
||||
.entry-detail-table tr:nth-child(even) {{
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}}
|
||||
.empty-placeholder {{
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
color: var(--text-sub);
|
||||
}}
|
||||
@media (max-width: 680px) {{
|
||||
.entry-card {{
|
||||
flex-direction: column;
|
||||
}}
|
||||
.entry-content h3 {{
|
||||
font-size: 18px;
|
||||
}}
|
||||
.meta-grid {{
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}}
|
||||
}}
|
||||
@media (max-width: 480px) {{
|
||||
.meta-grid {{
|
||||
grid-template-columns: 1fr;
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="hero">
|
||||
<h1 class="hero-title">{escape(page.title)}</h1>
|
||||
{subtitle_html}
|
||||
</header>
|
||||
<main>
|
||||
{metadata_html}
|
||||
{related_html}
|
||||
{sections_html}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class MessageSender:
|
||||
"""消息发送器"""
|
||||
|
||||
@@ -190,6 +800,198 @@ class BasicWPSInterface(PluginInterface):
|
||||
|
||||
|
||||
class WPSAPI(BasicWPSInterface):
|
||||
"""核心 WPS 插件基类,提供图鉴模板设施。"""
|
||||
|
||||
guide_section_labels: Dict[str, str] = {
|
||||
"commands": "指令一览",
|
||||
"items": "物品与资源",
|
||||
"recipes": "配方与合成",
|
||||
"guides": "系统指引",
|
||||
}
|
||||
|
||||
def get_guide_title(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return ""
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
return {}
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return ()
|
||||
|
||||
def collect_item_entries(self) -> Sequence[GuideEntry]:
|
||||
return ()
|
||||
|
||||
def collect_recipe_entries(self) -> Sequence[GuideEntry]:
|
||||
return ()
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return ()
|
||||
|
||||
def collect_additional_sections(self) -> Sequence[GuideSection]:
|
||||
return ()
|
||||
|
||||
def collect_guide_sections(self) -> Sequence[GuideSection]:
|
||||
sections: List[GuideSection] = []
|
||||
|
||||
command_entries = tuple(self.collect_command_entries())
|
||||
if command_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title=self.guide_section_labels["commands"],
|
||||
entries=command_entries,
|
||||
layout="list",
|
||||
)
|
||||
)
|
||||
|
||||
item_entries = tuple(self.collect_item_entries())
|
||||
if item_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title=self.guide_section_labels["items"],
|
||||
entries=item_entries,
|
||||
)
|
||||
)
|
||||
|
||||
recipe_entries = tuple(self.collect_recipe_entries())
|
||||
if recipe_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title=self.guide_section_labels["recipes"],
|
||||
entries=recipe_entries,
|
||||
)
|
||||
)
|
||||
|
||||
guide_entries = tuple(self.collect_guide_entries())
|
||||
if guide_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title=self.guide_section_labels["guides"],
|
||||
entries=guide_entries,
|
||||
layout="list",
|
||||
)
|
||||
)
|
||||
|
||||
additional_sections = tuple(self.collect_additional_sections())
|
||||
if additional_sections:
|
||||
sections.extend(additional_sections)
|
||||
|
||||
return tuple(sections)
|
||||
|
||||
def build_guide_page(self) -> GuidePage:
|
||||
metadata: Dict[str, str] = {}
|
||||
for key, value in self.get_guide_metadata().items():
|
||||
metadata[key] = str(value)
|
||||
|
||||
related = self.get_related_links()
|
||||
|
||||
return GuidePage(
|
||||
title=self.get_guide_title(),
|
||||
subtitle=self.get_guide_subtitle(),
|
||||
sections=self.collect_guide_sections(),
|
||||
metadata=metadata,
|
||||
related_links=related,
|
||||
)
|
||||
|
||||
def render_guide_page(self, page: GuidePage) -> str:
|
||||
return render_html_page(page)
|
||||
|
||||
def render_guide(self) -> str:
|
||||
return self.render_guide_page(self.build_guide_page())
|
||||
|
||||
def get_guide_response(self, content: str) -> HTMLResponse:
|
||||
return HTMLResponse(content)
|
||||
|
||||
@override
|
||||
def generate_router_illustrated_guide(self):
|
||||
async def handler() -> HTMLResponse:
|
||||
return self.get_guide_response(self.render_guide())
|
||||
|
||||
return handler
|
||||
|
||||
def get_related_links(self) -> Dict[str, Sequence[Dict[str, str]]]:
|
||||
links: Dict[str, Sequence[Dict[str, str]]] = {}
|
||||
|
||||
parents = []
|
||||
for base in self.__class__.__mro__[1:]:
|
||||
if not issubclass(base, WPSAPI):
|
||||
continue
|
||||
if base.__module__.startswith("Plugins."):
|
||||
parents.append(base)
|
||||
if base is WPSAPI:
|
||||
break
|
||||
if parents:
|
||||
parents_links = [self._build_class_link(cls) for cls in reversed(parents)]
|
||||
links["父类链"] = tuple(filter(None, parents_links))
|
||||
|
||||
child_links = [self._build_class_link(child) for child in self._iter_subclasses(self.__class__)]
|
||||
child_links = [link for link in child_links if link]
|
||||
if child_links:
|
||||
links["子类"] = tuple(child_links)
|
||||
|
||||
return links
|
||||
|
||||
def _build_class_link(self, cls: type) -> Optional[Dict[str, str]]:
|
||||
if not hasattr(cls, "__module__") or not cls.__module__.startswith("Plugins."):
|
||||
return None
|
||||
path = f"/api/{cls.__name__}"
|
||||
return {
|
||||
"label": cls.__name__,
|
||||
"href": path,
|
||||
}
|
||||
|
||||
def _iter_subclasses(self, cls: type) -> List[type]:
|
||||
collected: List[type] = []
|
||||
for subclass in cls.__subclasses__():
|
||||
if not issubclass(subclass, WPSAPI):
|
||||
continue
|
||||
if not subclass.__module__.startswith("Plugins."):
|
||||
continue
|
||||
collected.append(subclass)
|
||||
collected.extend(self._iter_subclasses(subclass))
|
||||
return collected
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "核心 Webhook 转发插件"
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
return {
|
||||
"Webhook 状态": "已配置" if MAIN_WEBHOOK_URL else "未配置",
|
||||
}
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "say",
|
||||
"identifier": "say",
|
||||
"description": "将后续消息内容以 Markdown 形式发送到主 Webhook。",
|
||||
"metadata": {"别名": "说"},
|
||||
"icon": "🗣️",
|
||||
"badge": "核心",
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "Webhook 绑定",
|
||||
"description": (
|
||||
"在项目配置中设置 `main_webhook_url` 后插件自动启用,"
|
||||
"所有注册的命令将调用 `send_markdown_message` 发送富文本。"
|
||||
),
|
||||
"icon": "🔗",
|
||||
},
|
||||
{
|
||||
"title": "消息格式",
|
||||
"description": (
|
||||
"默认使用 Markdown 模式发送,支持 `聊天ID` 与 `用户ID` 的 @ 提醒。"
|
||||
),
|
||||
"icon": "📝",
|
||||
},
|
||||
)
|
||||
|
||||
@override
|
||||
def is_enable_plugin(self) -> bool:
|
||||
if MAIN_WEBHOOK_URL == "":
|
||||
|
||||
@@ -13,7 +13,7 @@ from PWF.CoreModules.database import get_db, STATUS_COMPLETED
|
||||
from PWF.CoreModules.plugin_interface import DatabaseModel
|
||||
from PWF.CoreModules.flags import get_internal_debug
|
||||
|
||||
from .WPSAPI import WPSAPI
|
||||
from .WPSAPI import GuideEntry, GuideSection, WPSAPI
|
||||
from .WPSBackpackSystem import (
|
||||
BackpackItemDefinition,
|
||||
BackpackItemTier,
|
||||
@@ -73,6 +73,210 @@ class WPSAlchemyGame(WPSAPI):
|
||||
self._max_points_per_batch = MAX_POINTS_PER_BATCH
|
||||
logger.SaveProperties()
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "积分炼金与材料合成系统"
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
return {
|
||||
"配方数量": str(len(self._recipes)),
|
||||
"冷却时间(分钟)": str(self._cooldown_minutes),
|
||||
"单次积分上限": str(self._max_points_per_batch),
|
||||
}
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "炼金",
|
||||
"identifier": "炼金",
|
||||
"description": "投入积分或三件材料,等待冷却后获取结果。",
|
||||
"metadata": {"别名": "alchemy"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_item_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": self.ASH_ITEM_NAME,
|
||||
"identifier": self.ASH_ITEM_ID,
|
||||
"description": "炼金失败后获得的基础材料,可再次参与配方或出售。",
|
||||
},
|
||||
{
|
||||
"title": self.SLAG_ITEM_NAME,
|
||||
"identifier": self.SLAG_ITEM_ID,
|
||||
"description": "由 `炉灰` 合成,部分园艺/商店配方会引用该物品。",
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "积分炼金",
|
||||
"description": (
|
||||
f"`炼金 <积分>` 消耗积分尝试炼金,单次上限 {self._max_points_per_batch},"
|
||||
"结果与运势、阶段概率表 `_PHASE_TABLE` 相关。"
|
||||
),
|
||||
"icon": "💎",
|
||||
},
|
||||
{
|
||||
"title": "材料炼金",
|
||||
"description": (
|
||||
"`炼金 <材料1> <材料2> <材料3> [次数]` 支持批量执行,配方信息可通过 `炼金配方` 查询。"
|
||||
),
|
||||
"icon": "🧪",
|
||||
},
|
||||
{
|
||||
"title": "冷却与恢复",
|
||||
"description": (
|
||||
f"默认冷却 {self._cooldown_minutes} 分钟,任务调度用于结算完成;"
|
||||
"重启后会自动恢复未结算记录。"
|
||||
),
|
||||
"icon": "⏲️",
|
||||
},
|
||||
)
|
||||
|
||||
def collect_additional_sections(self) -> Sequence[GuideSection]:
|
||||
sections = list(super().collect_additional_sections())
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="基础配方",
|
||||
entries=self._build_core_recipes(),
|
||||
layout="grid",
|
||||
section_id="alchemy-core",
|
||||
description="系统内置的基础配方,可在没有额外模块时直接使用。",
|
||||
)
|
||||
)
|
||||
|
||||
garden_recipes = self._build_garden_wine_recipes()
|
||||
if garden_recipes:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="果酒配方",
|
||||
entries=garden_recipes,
|
||||
layout="grid",
|
||||
section_id="alchemy-garden",
|
||||
description="菜园系统提供的果酒炼金配方,使用三份果实即可酿造果酒。",
|
||||
)
|
||||
)
|
||||
|
||||
crystal_recipes = self._build_crystal_chain_recipes()
|
||||
if crystal_recipes:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="水晶链路",
|
||||
entries=crystal_recipes,
|
||||
layout="list",
|
||||
section_id="alchemy-crystal",
|
||||
description="水晶系统的多阶段炼金链路,最终可获得黑水晶核心。",
|
||||
)
|
||||
)
|
||||
|
||||
return tuple(sections)
|
||||
|
||||
def _build_core_recipes(self) -> Sequence[GuideEntry]:
|
||||
entries: List[GuideEntry] = []
|
||||
entries.append(
|
||||
GuideEntry(
|
||||
title="炉灰 → 炉渣",
|
||||
identifier=f"{self.ASH_ITEM_ID} × 3",
|
||||
description="循环利用基础产物,将多余炉灰转化为更稀有的炉渣。",
|
||||
category="基础配方",
|
||||
metadata={
|
||||
"成功率": "100%",
|
||||
"失败产物": self.ASH_ITEM_ID,
|
||||
},
|
||||
icon="🔥",
|
||||
details=[
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
f"材料:{self.ASH_ITEM_ID} × 3",
|
||||
f"成功产物:{self.SLAG_ITEM_ID}",
|
||||
f"失败产物:{self.ASH_ITEM_ID}",
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
def _build_garden_wine_recipes(self) -> Sequence[GuideEntry]:
|
||||
try:
|
||||
from Plugins.WPSGardenSystem.garden_models import GARDEN_CROPS
|
||||
except ImportError:
|
||||
return ()
|
||||
entries: List[GuideEntry] = []
|
||||
for crop in GARDEN_CROPS.values():
|
||||
if not crop.wine_item_id:
|
||||
continue
|
||||
items = [
|
||||
f"材料:{crop.fruit_id} × 3",
|
||||
f"成功产物:{crop.wine_item_id}",
|
||||
"失败产物:garden_item_rot_fruit",
|
||||
"基础成功率:75%",
|
||||
]
|
||||
entries.append(
|
||||
GuideEntry(
|
||||
title=f"{crop.display_name}果酒",
|
||||
identifier=f"{crop.fruit_id} ×3",
|
||||
description=f"使用 {crop.display_name} 的果实炼制同名果酒。",
|
||||
category="果酒配方",
|
||||
metadata={
|
||||
"果酒稀有度": crop.wine_tier or "rare",
|
||||
},
|
||||
icon="🍷",
|
||||
tags=(crop.tier.title(),),
|
||||
details=[{"type": "list", "items": items}],
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
def _build_crystal_chain_recipes(self) -> Sequence[GuideEntry]:
|
||||
try:
|
||||
from Plugins.WPSCrystalSystem.crystal_models import (
|
||||
DEFAULT_CRYSTAL_COLOR_MAP,
|
||||
DEFAULT_CRYSTAL_EXCHANGE_ENTRIES,
|
||||
)
|
||||
except ImportError:
|
||||
return ()
|
||||
entries: List[GuideEntry] = []
|
||||
for color_def in DEFAULT_CRYSTAL_COLOR_MAP.values():
|
||||
stage_details: List[str] = []
|
||||
for stage in color_def.chain_stages:
|
||||
mats = " + ".join(stage.materials)
|
||||
stage_details.append(
|
||||
f"{stage.identifier}:{mats} → {stage.result_item}(成功率 {stage.base_success_rate*100:.0f}%)"
|
||||
)
|
||||
stage_details.append(
|
||||
f"等待阶段:消耗 {', '.join(f'{k}×{v}' for k, v in color_def.wait_stage.consumed_items.items())},"
|
||||
f"耗时 {color_def.wait_stage.delay_minutes} 分钟"
|
||||
)
|
||||
fusion = color_def.final_fusion
|
||||
stage_details.append(
|
||||
f"最终融合:{', '.join(fusion.materials)} → {fusion.result_item}(成功率 {fusion.base_success_rate*100:.0f}%)"
|
||||
)
|
||||
entries.append(
|
||||
GuideEntry(
|
||||
title=color_def.display_name,
|
||||
identifier=color_def.color_key,
|
||||
description="水晶染色与融合的完整链路。",
|
||||
category="水晶链路",
|
||||
icon="💠",
|
||||
details=[{"type": "list", "items": stage_details}],
|
||||
)
|
||||
)
|
||||
for exchange in DEFAULT_CRYSTAL_EXCHANGE_ENTRIES.values():
|
||||
items = ", ".join(f"{item_id}×{qty}" for item_id, qty in exchange.required_items.items())
|
||||
entries.append(
|
||||
GuideEntry(
|
||||
title=exchange.metadata.get("display_name", exchange.identifier),
|
||||
identifier=exchange.identifier,
|
||||
description=f"兑换奖励:{exchange.reward_item}",
|
||||
category="水晶兑换",
|
||||
icon="🔁",
|
||||
details=[f"需求:{items}"],
|
||||
)
|
||||
)
|
||||
return entries
|
||||
@override
|
||||
def dependencies(self) -> List[type]:
|
||||
return [WPSAPI, WPSBackpackSystem, WPSConfigAPI, WPSFortuneSystem, WPSStoreSystem]
|
||||
@@ -858,6 +1062,31 @@ class WPSAlchemyRecipeLookup(WPSAPI):
|
||||
self._alchemy: Optional[WPSAlchemyGame] = None
|
||||
self._backpack: Optional[WPSBackpackSystem] = None
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "查询指定物品涉及的炼金配方"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "炼金配方",
|
||||
"identifier": "炼金配方",
|
||||
"description": "展示物品作为材料、成功产物或失败产物的所有配方。",
|
||||
"metadata": {"别名": "alchemy_recipe"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "查询格式",
|
||||
"description": "`炼金配方 <物品ID>` 或 `炼金配方 <物品名称>`,忽略大小写。",
|
||||
},
|
||||
{
|
||||
"title": "输出结构",
|
||||
"description": "结果按材料/成功/失败三类分组,列出配方材料与成功率。",
|
||||
},
|
||||
)
|
||||
|
||||
def dependencies(self) -> List[type]:
|
||||
return [WPSAlchemyGame, WPSBackpackSystem]
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, override
|
||||
from typing import Dict, List, Optional, Sequence, override
|
||||
|
||||
from PWF.Convention.Runtime.Architecture import Architecture
|
||||
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
|
||||
from PWF.CoreModules.database import get_db
|
||||
from PWF.CoreModules.flags import get_internal_debug
|
||||
|
||||
from .WPSAPI import WPSAPI
|
||||
from .WPSAPI import GuideEntry, GuideSection, WPSAPI
|
||||
|
||||
logger: ProjectConfig = Architecture.Get(ProjectConfig)
|
||||
|
||||
@@ -58,6 +58,48 @@ class WPSBackpackSystem(WPSAPI):
|
||||
ITEMS_TABLE = "backpack_items"
|
||||
USER_ITEMS_TABLE = "backpack_user_items"
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "管理物品注册、背包存储与查询的核心系统"
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
return {
|
||||
"物品缓存数": str(len(self._item_cache)),
|
||||
"数据表": f"{self.ITEMS_TABLE}, {self.USER_ITEMS_TABLE}",
|
||||
}
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "背包",
|
||||
"identifier": "背包",
|
||||
"description": "以稀有度分组展示用户当前携带物品。",
|
||||
"metadata": {"别名": "backpack"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
tier_labels = ", ".join(tier.display_name for tier in BackpackItemTier)
|
||||
return (
|
||||
{
|
||||
"title": "物品注册",
|
||||
"description": (
|
||||
"`register_item(item_id, name, tier, description)` "
|
||||
"将物品写入背包表,重复调用会更新名称、稀有度和描述。"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "稀有度体系",
|
||||
"description": f"支持稀有度:{tier_labels},调用 `to_markdown_label` 可渲染彩色标签。",
|
||||
},
|
||||
{
|
||||
"title": "库存操作",
|
||||
"description": (
|
||||
"`add_item` / `set_item_quantity` / `_get_user_quantity` "
|
||||
"确保用户物品数量保持非负,并自动创建记录。"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._item_cache: Dict[str, BackpackItemDefinition] = {}
|
||||
@@ -124,6 +166,50 @@ class WPSBackpackSystem(WPSAPI):
|
||||
description=description,
|
||||
)
|
||||
|
||||
def _iter_registered_items(self) -> Sequence[BackpackItemDefinition]:
|
||||
try:
|
||||
if not self._item_cache:
|
||||
self._warm_item_cache()
|
||||
except Exception:
|
||||
return ()
|
||||
return tuple(self._item_cache.values())
|
||||
|
||||
def collect_additional_sections(self) -> Sequence[GuideSection]:
|
||||
sections = list(super().collect_additional_sections())
|
||||
item_entries: List[GuideEntry] = []
|
||||
tier_icons = {
|
||||
BackpackItemTier.COMMON: "🪙",
|
||||
BackpackItemTier.RARE: "💠",
|
||||
BackpackItemTier.EPIC: "⚡",
|
||||
BackpackItemTier.LEGENDARY: "🌟",
|
||||
}
|
||||
for definition in self._iter_registered_items():
|
||||
item_entries.append(
|
||||
GuideEntry(
|
||||
title=definition.name,
|
||||
identifier=definition.item_id,
|
||||
description=definition.description or "(暂无描述)",
|
||||
category="背包物品",
|
||||
metadata={
|
||||
"稀有度": definition.tier.display_name,
|
||||
},
|
||||
icon=tier_icons.get(definition.tier, "🎁"),
|
||||
tags=(definition.tier.display_name,),
|
||||
group=definition.tier.display_name,
|
||||
)
|
||||
)
|
||||
if item_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="物品图鉴",
|
||||
entries=item_entries,
|
||||
layout="grid",
|
||||
section_id="backpack-items",
|
||||
description="当前已注册的背包物品列表,按稀有度分组展示。",
|
||||
)
|
||||
)
|
||||
return tuple(sections)
|
||||
|
||||
# region 对外接口
|
||||
|
||||
def register_item(
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
|
||||
|
||||
from Plugins.WPSAPI import GuideEntry, GuideSection
|
||||
from .combat_plugin_base import WPSCombatBase
|
||||
from .combat_models import CombatConfig
|
||||
|
||||
|
||||
logger: ProjectConfig = ProjectConfig()
|
||||
@@ -16,6 +18,133 @@ logger: ProjectConfig = ProjectConfig()
|
||||
class WPSCombatAdventure(WPSCombatBase):
|
||||
"""冒险系统插件"""
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "阶段式 PVE 冒险,产出装备与素材"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="冒险 开始",
|
||||
identifier="冒险 开始 [食物...]",
|
||||
description="启动第 1 阶段冒险,可额外投入食物缩短时间并提升成功率。",
|
||||
metadata={"别名": "start"},
|
||||
icon="🚀",
|
||||
tags=("阶段1", "需未受伤"),
|
||||
details=[
|
||||
{
|
||||
"type": "steps",
|
||||
"items": [
|
||||
"检查自身状态,确认未处于受伤或其他冒险中。",
|
||||
"可选:准备食物 `food_id` 列表;每个食物将被立即消耗。",
|
||||
"系统计算预计耗时、成功率并生成冒险记录。",
|
||||
"创建时钟任务,时间到后自动结算。",
|
||||
],
|
||||
},
|
||||
"结算时会根据装备强度与运势发放奖励。",
|
||||
],
|
||||
),
|
||||
GuideEntry(
|
||||
title="继续冒险",
|
||||
identifier="冒险 / 继续冒险",
|
||||
description="在已有链路下进入下一阶段或查看剩余时间。",
|
||||
metadata={"别名": "adventure / continue"},
|
||||
icon="⏱️",
|
||||
tags=("阶段推进",),
|
||||
details=[
|
||||
{
|
||||
"type": "steps",
|
||||
"items": [
|
||||
"查询当前冒险记录,若已在倒计时阶段则返回剩余时间。",
|
||||
"如冒险链完成并准备进入下一阶段,可再次投喂食物并启动。",
|
||||
"系统保持阶段编号,累计奖励与时间。",
|
||||
],
|
||||
}
|
||||
],
|
||||
),
|
||||
GuideEntry(
|
||||
title="冒险 停止",
|
||||
identifier="冒险 停止 / 放弃",
|
||||
description="放弃当前冒险链,结算状态并清空倒计时。",
|
||||
metadata={"别名": "放弃 / 停止"},
|
||||
icon="🛑",
|
||||
tags=("风险",),
|
||||
details=[
|
||||
"放弃后当前阶段奖励作废,未来可从第 1 阶段重开。",
|
||||
"若系统已开始结算,则命令会提示等待完成。",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="冒险阶段",
|
||||
description="Adventure 链路包含多个阶段,难度逐渐提升,奖励也随之增加。",
|
||||
icon="⚙️",
|
||||
details=[
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"阶段时间:基础 15 分钟,最高不超过 24 小时。",
|
||||
"投喂食物:每份食物提供额外时间与成功率加成。",
|
||||
"装备影响:装备强度越高,成功率越高且时间越短。",
|
||||
"运势影响:由运势系统提供当前整点的幸运值,用于修正成功率。",
|
||||
],
|
||||
}
|
||||
],
|
||||
),
|
||||
GuideEntry(
|
||||
title="奖励构成",
|
||||
description="冒险完成后会发放积分、装备、材料、纪念品等。",
|
||||
icon="🎁",
|
||||
details=[
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"基础积分奖励随阶段提升。",
|
||||
"装备掉落根据稀有度加权抽取。",
|
||||
"部分阶段触发事件可获得药剂、种子或纪念品。",
|
||||
],
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def collect_additional_sections(self) -> Sequence[GuideSection]:
|
||||
sections = list(super().collect_additional_sections())
|
||||
adventure_config_entries: List[GuideEntry] = []
|
||||
config_labels = {
|
||||
"combat_adventure_base_time": "阶段起始时间 (分钟)",
|
||||
"combat_adventure_max_time": "时间上限 (分钟)",
|
||||
"combat_food_support_time": "单个食物提供时间 (分钟)",
|
||||
"combat_adventure_base_success_rate": "基础成功率",
|
||||
"combat_adventure_stage_penalty": "阶段成功率衰减",
|
||||
"combat_adventure_equipment_coeff": "装备强度加成系数",
|
||||
"combat_adventure_fortune_coeff": "运势加成系数",
|
||||
"combat_time_reduction_divisor": "时间缩减除数",
|
||||
}
|
||||
for key, label in config_labels.items():
|
||||
value = CombatConfig.get(key)
|
||||
adventure_config_entries.append(
|
||||
GuideEntry(
|
||||
title=label,
|
||||
identifier=key,
|
||||
description=f"{value}",
|
||||
category="配置项",
|
||||
icon="📐",
|
||||
)
|
||||
)
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="冒险参数一览",
|
||||
entries=adventure_config_entries,
|
||||
layout="grid",
|
||||
section_id="adventure-config",
|
||||
description="核心公式与系数决定冒险耗时、成功率与奖励结构。",
|
||||
)
|
||||
)
|
||||
return tuple(sections)
|
||||
|
||||
def is_enable_plugin(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Type
|
||||
from typing import Dict, List, Type, Union
|
||||
|
||||
from PWF.Convention.Runtime.Architecture import Architecture
|
||||
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
|
||||
from PWF.CoreModules.plugin_interface import DatabaseModel
|
||||
|
||||
from Plugins.WPSAPI import WPSAPI
|
||||
from Plugins.WPSAPI import GuideEntry, GuideSection, WPSAPI
|
||||
from Plugins.WPSBackpackSystem import BackpackItemTier, WPSBackpackSystem
|
||||
from Plugins.WPSStoreSystem import WPSStoreSystem
|
||||
from Plugins.WPSConfigSystem import WPSConfigAPI
|
||||
@@ -46,6 +46,270 @@ class WPSCombatBase(WPSAPI):
|
||||
|
||||
_service: CombatService | None = None
|
||||
_initialized: bool = False
|
||||
|
||||
def collect_additional_sections(self) -> Sequence[GuideSection]:
|
||||
sections = list(super().collect_additional_sections())
|
||||
|
||||
equipment_entries = self._build_equipment_entries()
|
||||
if equipment_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="装备详单",
|
||||
entries=equipment_entries,
|
||||
layout="grid",
|
||||
section_id="combat-equipment",
|
||||
description="战斗系统预置的装备清单及属性效果。",
|
||||
)
|
||||
)
|
||||
|
||||
potion_entries = self._build_simple_item_entries(
|
||||
COMBAT_POTIONS,
|
||||
category="战斗药剂",
|
||||
)
|
||||
if potion_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="药剂与增益",
|
||||
entries=potion_entries,
|
||||
layout="grid",
|
||||
section_id="combat-potions",
|
||||
description="药剂品质影响价格与效果,部分由冒险掉落。",
|
||||
)
|
||||
)
|
||||
|
||||
material_entries = self._build_simple_item_entries(
|
||||
ADVENTURE_MATERIALS,
|
||||
category="冒险材料",
|
||||
)
|
||||
if material_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="冒险材料",
|
||||
entries=material_entries,
|
||||
layout="grid",
|
||||
section_id="combat-materials",
|
||||
description="材料主要来源于冒险战斗,稀有度决定获取概率。",
|
||||
)
|
||||
)
|
||||
|
||||
souvenir_entries = self._build_souvenir_entries()
|
||||
if souvenir_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="纪念品一览",
|
||||
entries=souvenir_entries,
|
||||
layout="grid",
|
||||
section_id="combat-souvenirs",
|
||||
description="纪念品可在营地或商店出售兑换积分。",
|
||||
)
|
||||
)
|
||||
|
||||
seed_entries = self._build_simple_item_entries(
|
||||
ADVENTURE_SEEDS,
|
||||
category="冒险种子",
|
||||
)
|
||||
if seed_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="冒险种子",
|
||||
entries=seed_entries,
|
||||
layout="grid",
|
||||
section_id="combat-seeds",
|
||||
description="冒险专属种子,可在菜园种植获取增益或任务物资。",
|
||||
)
|
||||
)
|
||||
|
||||
skill_entries = self._build_skill_entries()
|
||||
if skill_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="技能图鉴",
|
||||
entries=skill_entries,
|
||||
layout="list",
|
||||
section_id="combat-skills",
|
||||
description="装备附带的技能可在战斗中释放,冷却时间以回合计算。",
|
||||
)
|
||||
)
|
||||
|
||||
return tuple(sections)
|
||||
|
||||
def _format_tier(self, tier: BackpackItemTier) -> str:
|
||||
return f"{tier.display_name}"
|
||||
|
||||
def _build_equipment_entries(self) -> List[GuideEntry]:
|
||||
entries: List[GuideEntry] = []
|
||||
attr_names = {
|
||||
"HP": "生命值",
|
||||
"ATK": "攻击力",
|
||||
"DEF": "防御力",
|
||||
"SPD": "速度",
|
||||
"CRIT": "暴击率",
|
||||
"CRIT_DMG": "暴击伤害",
|
||||
}
|
||||
tier_icons = {
|
||||
BackpackItemTier.COMMON: "⚔️",
|
||||
BackpackItemTier.RARE: "🛡️",
|
||||
BackpackItemTier.EPIC: "🔥",
|
||||
BackpackItemTier.LEGENDARY: "🌟",
|
||||
}
|
||||
for eq in EQUIPMENT_REGISTRY.values():
|
||||
attr_list = []
|
||||
for key, value in eq.attributes.items():
|
||||
label = attr_names.get(key, key)
|
||||
suffix = "%" if key in ("CRIT", "CRIT_DMG") else ""
|
||||
attr_list.append(f"{label}+{value}{suffix}")
|
||||
skills = []
|
||||
for skill_id in eq.skill_ids:
|
||||
skill = SKILL_REGISTRY.get(skill_id)
|
||||
if skill:
|
||||
skills.append(f"{skill.icon} {skill.name}")
|
||||
metadata = {
|
||||
"槽位": eq.slot,
|
||||
"稀有度": self._format_tier(eq.tier),
|
||||
}
|
||||
details: List[Dict[str, Any] | str] = []
|
||||
if attr_list:
|
||||
details.append({"type": "list", "items": attr_list})
|
||||
if skills:
|
||||
details.append({"type": "list", "items": [f"技能:{info}" for info in skills]})
|
||||
if eq.description:
|
||||
details.append(eq.description)
|
||||
entries.append(
|
||||
GuideEntry(
|
||||
title=eq.name,
|
||||
identifier=eq.item_id,
|
||||
description=eq.description or "标准装备。",
|
||||
category="装备",
|
||||
metadata=metadata,
|
||||
icon=tier_icons.get(eq.tier, "⚔️"),
|
||||
tags=skills,
|
||||
details=details,
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
def _build_simple_item_entries(
|
||||
self,
|
||||
registry: Dict[str, tuple],
|
||||
*,
|
||||
category: str,
|
||||
) -> List[GuideEntry]:
|
||||
entries: List[GuideEntry] = []
|
||||
icon_map = {
|
||||
"战斗药剂": "🧪",
|
||||
"冒险材料": "🪨",
|
||||
"纪念品": "🎖️",
|
||||
"冒险种子": "🌱",
|
||||
}
|
||||
for item_id, payload in registry.items():
|
||||
name, tier, *rest = payload
|
||||
description = rest[-1] if rest else ""
|
||||
metadata = {"稀有度": self._format_tier(tier)}
|
||||
if category == "战斗药剂":
|
||||
price_lookup = {
|
||||
BackpackItemTier.COMMON: 50,
|
||||
BackpackItemTier.RARE: 150,
|
||||
BackpackItemTier.EPIC: 500,
|
||||
}
|
||||
price = price_lookup.get(tier)
|
||||
if price:
|
||||
metadata["默认售价"] = f"{price} 分"
|
||||
entries.append(
|
||||
GuideEntry(
|
||||
title=name,
|
||||
identifier=item_id,
|
||||
description=description,
|
||||
category=category,
|
||||
metadata=metadata,
|
||||
icon=icon_map.get(category, "📦"),
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
def _build_souvenir_entries(self) -> List[GuideEntry]:
|
||||
entries: List[GuideEntry] = []
|
||||
for item_id, (name, tier, price, desc) in ADVENTURE_SOUVENIRS.items():
|
||||
entries.append(
|
||||
GuideEntry(
|
||||
title=name,
|
||||
identifier=item_id,
|
||||
description=desc,
|
||||
category="纪念品",
|
||||
metadata={
|
||||
"稀有度": self._format_tier(tier),
|
||||
"基础售价": f"{price} 分",
|
||||
},
|
||||
icon="🎖️",
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
def _build_skill_entries(self) -> List[GuideEntry]:
|
||||
entries: List[GuideEntry] = []
|
||||
for skill in SKILL_REGISTRY.values():
|
||||
details: List[Union[str, Dict[str, Any]]] = [
|
||||
{"type": "list", "items": [effect.get("description", str(effect)) for effect in skill.effects]},
|
||||
]
|
||||
if skill.cooldown:
|
||||
details.append(f"冷却:{skill.cooldown} 回合")
|
||||
entries.append(
|
||||
GuideEntry(
|
||||
title=skill.name,
|
||||
identifier=skill.skill_id,
|
||||
description=skill.description,
|
||||
category="技能",
|
||||
icon=skill.icon,
|
||||
metadata={},
|
||||
details=details,
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "冒险、战斗与装备体系的基础能力"
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
return {
|
||||
"装备数量": str(len(EQUIPMENT_REGISTRY)),
|
||||
"药剂数量": str(len(COMBAT_POTIONS)),
|
||||
"纪念品数量": str(len(ADVENTURE_SOUVENIRS)),
|
||||
"冒险材料": str(len(ADVENTURE_MATERIALS)),
|
||||
}
|
||||
|
||||
def collect_item_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "装备库",
|
||||
"description": f"{len(EQUIPMENT_REGISTRY)} 件装备,自动注册至背包并带有属性描述。",
|
||||
},
|
||||
{
|
||||
"title": "药剂与材料",
|
||||
"description": (
|
||||
f"{len(COMBAT_POTIONS)} 种药剂、{len(ADVENTURE_MATERIALS)} 种冒险素材,"
|
||||
"部分可在商店购买或冒险获得。"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "纪念品",
|
||||
"description": f"{len(ADVENTURE_SOUVENIRS)} 种纪念品可在营地出售换取积分。",
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "冒险流程",
|
||||
"description": "冒险按阶段推进,使用食物缩短时间并依赖运势与装备强度影响结果。",
|
||||
},
|
||||
{
|
||||
"title": "装备体系",
|
||||
"description": "装备提供属性与技能加成,通过 `装备`/`战斗属性` 等指令查看详情。",
|
||||
},
|
||||
{
|
||||
"title": "积分与资源",
|
||||
"description": "冒险和战斗会产出积分、装备、材料等,通过商店与营地完成循环。",
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def service(cls) -> CombatService:
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
|
||||
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from .combat_plugin_base import WPSCombatBase
|
||||
|
||||
|
||||
@@ -15,6 +16,69 @@ logger: ProjectConfig = ProjectConfig()
|
||||
class WPSCombatBattle(WPSCombatBase):
|
||||
"""PVP对战插件"""
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "玩家间回合制对战指令集"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="挑战",
|
||||
identifier="挑战 <目标用户ID>",
|
||||
description="向指定玩家发起 PVP 挑战。",
|
||||
metadata={"别名": "challenge"},
|
||||
icon="⚔️",
|
||||
details=[
|
||||
{"type": "list", "items": ["不可挑战自己。", "挑战在超时前需对方接受,否则自动失效。"]},
|
||||
],
|
||||
),
|
||||
GuideEntry(
|
||||
title="接受挑战",
|
||||
identifier="接受挑战 <挑战ID>",
|
||||
description="接受待处理的挑战并初始化战斗。",
|
||||
metadata={"别名": "accept"},
|
||||
icon="✅",
|
||||
details=[
|
||||
{"type": "steps", "items": ["输入挑战列表中的 ID。", "系统创建战斗记录并通知双方。"]},
|
||||
],
|
||||
),
|
||||
GuideEntry(
|
||||
title="拒绝挑战",
|
||||
identifier="拒绝挑战 <挑战ID>",
|
||||
description="拒绝尚未开始的挑战请求。",
|
||||
metadata={"别名": "reject"},
|
||||
icon="🚫",
|
||||
),
|
||||
GuideEntry(
|
||||
title="战斗动作",
|
||||
identifier="战斗 <战斗ID> <技能名>",
|
||||
description="在战斗中释放技能或执行普攻。",
|
||||
metadata={"别名": "battle"},
|
||||
icon="🌀",
|
||||
details=[
|
||||
{"type": "list", "items": ["技能冷却与效果可在 `技能列表` 中查看。", "战斗为回合制,按顺序执行。"]},
|
||||
],
|
||||
),
|
||||
GuideEntry(
|
||||
title="投降",
|
||||
identifier="投降 <战斗ID>",
|
||||
description="主动认输并结束当前战斗。",
|
||||
metadata={"别名": "surrender"},
|
||||
icon="🏳️",
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "挑战生命周期",
|
||||
"description": "挑战需在配置的超时前被接受,超时将自动失效。",
|
||||
},
|
||||
{
|
||||
"title": "战斗指令",
|
||||
"description": "战斗中按回合输入技能,系统根据属性与技能效果计算伤害。",
|
||||
},
|
||||
)
|
||||
|
||||
def is_enable_plugin(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.Architecture import Architecture
|
||||
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
|
||||
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from Plugins.WPSBackpackSystem import WPSBackpackSystem
|
||||
from Plugins.WPSConfigSystem import WPSConfigAPI
|
||||
|
||||
@@ -27,6 +28,35 @@ class WPSCombatCamp(WPSCombatBase):
|
||||
meta[0].lower(): item_id for item_id, meta in self._souvenir_by_id.items()
|
||||
}
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "营地寄售区:纪念品快速变现"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="营地 售出",
|
||||
identifier="营地 售出 <纪念品> <数量>",
|
||||
description="将纪念品出售给系统换取积分。",
|
||||
metadata={"别名": "camp"},
|
||||
icon="🏕️",
|
||||
details=[
|
||||
{"type": "steps", "items": ["输入纪念品名称或 ID。", "校验背包数量并扣除。", "根据基础售价发放积分。"]},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "销售指令",
|
||||
"description": "`营地 售出 <纪念品> <数量>`,名称可用中文或物品ID。",
|
||||
},
|
||||
{
|
||||
"title": "积分结算",
|
||||
"description": "根据纪念品基础售价乘以数量发放积分,并扣除背包库存。",
|
||||
},
|
||||
)
|
||||
|
||||
def is_enable_plugin(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
|
||||
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from .combat_plugin_base import WPSCombatBase
|
||||
|
||||
|
||||
@@ -15,6 +16,42 @@ logger: ProjectConfig = ProjectConfig()
|
||||
class WPSCombatEquipment(WPSCombatBase):
|
||||
"""装备管理插件"""
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "穿戴与卸下战斗装备"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="装备",
|
||||
identifier="装备 [物品名|ID]",
|
||||
description="穿戴指定装备,或不带参数时查看当前装备。",
|
||||
metadata={"别名": "equip"},
|
||||
icon="🛡️",
|
||||
details=[
|
||||
{"type": "list", "items": ["名称与 ID 均可匹配装备库条目。", "若未携带该装备会提示不足。"]},
|
||||
],
|
||||
),
|
||||
GuideEntry(
|
||||
title="卸下",
|
||||
identifier="卸下 <槽位>",
|
||||
description="卸下指定槽位(weapon/helmet/armor/boots/accessory)的装备。",
|
||||
metadata={"别名": "unequip"},
|
||||
icon="🧤",
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "槽位说明",
|
||||
"description": "支持 weapon/helmet/armor/boots/accessory 五个槽位。",
|
||||
},
|
||||
{
|
||||
"title": "属性加成",
|
||||
"description": "装备属性直接影响战斗与冒险计算,可在 `战斗属性` 指令中查看综合加成。",
|
||||
},
|
||||
)
|
||||
|
||||
def is_enable_plugin(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from Plugins.WPSCombatSystem.combat_models import CombatConfig
|
||||
|
||||
from .combat_plugin_base import WPSCombatBase
|
||||
|
||||
@@ -15,6 +17,32 @@ logger: ProjectConfig = ProjectConfig()
|
||||
class WPSCombatHeal(WPSCombatBase):
|
||||
"""治疗系统插件"""
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "恢复受伤状态并重返冒险"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="治疗",
|
||||
identifier="治疗",
|
||||
description="消耗积分清除受伤状态,使角色可继续冒险或战斗。",
|
||||
metadata={"别名": "heal / 恢复"},
|
||||
icon="💊",
|
||||
details=[
|
||||
{"type": "list", "items": ["若未受伤会返回提示信息。", "扣除积分后立即清除受伤标记。"]},
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
heal_cost = CombatConfig.get_int("combat_heal_cost")
|
||||
return (
|
||||
{
|
||||
"title": "费用计算",
|
||||
"description": f"治疗费用由配置 `combat_heal_cost` 决定,当前为 {heal_cost} 分。",
|
||||
},
|
||||
)
|
||||
|
||||
def is_enable_plugin(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.Config import *
|
||||
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
|
||||
from Plugins.WPSCombatSystem.combat_models import PlayerStats, EquipmentDefinition
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from Plugins.WPSCombatSystem.combat_models import EquipmentDefinition, PlayerStats
|
||||
from .combat_plugin_base import WPSCombatBase
|
||||
|
||||
|
||||
@@ -16,6 +17,44 @@ logger: ProjectConfig = ProjectConfig()
|
||||
class WPSCombatStatus(WPSCombatBase):
|
||||
"""状态查看插件"""
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "查看战斗属性、装备栏与技能列表"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="战斗属性",
|
||||
identifier="战斗属性",
|
||||
description="展示基础属性、装备强度与技能概览。",
|
||||
metadata={"别名": "combat"},
|
||||
icon="📊",
|
||||
),
|
||||
GuideEntry(
|
||||
title="装备栏",
|
||||
identifier="装备栏",
|
||||
description="仅查看当前装备与各槽位详情。",
|
||||
icon="🎽",
|
||||
),
|
||||
GuideEntry(
|
||||
title="技能列表",
|
||||
identifier="技能列表",
|
||||
description="罗列当前可用技能、描述与冷却时间。",
|
||||
icon="📜",
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "属性来源",
|
||||
"description": "数值由基础配置、装备加成与技能被动共同组成。",
|
||||
},
|
||||
{
|
||||
"title": "技能冷却",
|
||||
"description": "输出中会标注技能冷却回合,用于 PVP 战斗操作参考。",
|
||||
},
|
||||
)
|
||||
|
||||
def is_enable_plugin(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@@ -14,6 +14,41 @@ CHECKIN_POINTS = logger.FindItem("checkin_points", 100)
|
||||
logger.SaveProperties()
|
||||
|
||||
class WPSConfigAPI(WPSAPI):
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "用户基础资料与积分配置接口"
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
return {
|
||||
"数据表": "user_info",
|
||||
"每日签到积分": str(CHECKIN_POINTS),
|
||||
}
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "config",
|
||||
"identifier": "config",
|
||||
"description": "配置与查询用户昵称、URL、积分等信息。",
|
||||
"metadata": {"别名": "cfg"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "设置指令",
|
||||
"description": "`config set user.name <昵称>` / `config set user.url <URL>`",
|
||||
},
|
||||
{
|
||||
"title": "查询指令",
|
||||
"description": "`config get user.name|user.url|user.point` 返回当前资料或积分。",
|
||||
},
|
||||
{
|
||||
"title": "数据校验",
|
||||
"description": "内部自动确保用户记录存在,并限制昵称长度与 URL 前缀。",
|
||||
},
|
||||
)
|
||||
|
||||
@override
|
||||
def dependencies(self) -> List[Type]:
|
||||
return [WPSAPI]
|
||||
@@ -196,6 +231,37 @@ class WPSConfigAPI(WPSAPI):
|
||||
|
||||
|
||||
class WPSCheckinAPI(WPSAPI):
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "每日签到并发放积分的快捷指令"
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
return {
|
||||
"数据表": "daily_checkin",
|
||||
"签到积分": str(CHECKIN_POINTS),
|
||||
}
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "checkin",
|
||||
"identifier": "checkin",
|
||||
"description": "执行签到流程,发放积分并反馈今日进度。",
|
||||
"metadata": {"别名": "签到 / 积分"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "签到逻辑",
|
||||
"description": "同一日多次调用仅第一次成功,并记录在 `daily_checkin` 表内。",
|
||||
},
|
||||
{
|
||||
"title": "积分结算",
|
||||
"description": "成功签到将通过 `WPSConfigAPI.adjust_user_points` 增加积分。",
|
||||
},
|
||||
)
|
||||
|
||||
@override
|
||||
def dependencies(self) -> List[Type]:
|
||||
return [WPSAPI]
|
||||
|
||||
@@ -53,6 +53,54 @@ class WPSCrystalSystem(WPSAPI):
|
||||
key.lower(): value for key, value in DEFAULT_CRYSTAL_EXCHANGE_ENTRIES.items()
|
||||
}
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "水晶养成、颜色淬炼与兑换拓展系统"
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
return {
|
||||
"颜色数量": str(len(self._colors)),
|
||||
"水晶物品": str(len(self._items)),
|
||||
"兑换项目": str(len(self._exchange_entries)),
|
||||
}
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "水晶",
|
||||
"identifier": "水晶",
|
||||
"description": "查看系统配置或执行变色与兑换等操作。",
|
||||
"metadata": {"别名": "crystal"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_item_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "基础水晶物品",
|
||||
"description": f"{len(self._items)} 个水晶组件,可由背包/商店系统持有与交易。",
|
||||
},
|
||||
{
|
||||
"title": "颜色链路",
|
||||
"description": f"{len(self._colors)} 条变色链(包含等待阶段与最终融合)。",
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "变色流程",
|
||||
"description": "`水晶 变色 <颜色>` 进入等待流程,完成后获得对应水晶部件。",
|
||||
},
|
||||
{
|
||||
"title": "兑换指令",
|
||||
"description": "`水晶 兑换 <ID>` 消耗配置好的材料换取奖励物品。",
|
||||
},
|
||||
{
|
||||
"title": "菜园扩展",
|
||||
"description": "系统会向菜园注册水晶树作物,使果实与水晶体系互相联动。",
|
||||
},
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Plugin lifecycle
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -32,6 +32,35 @@ _FORTUNE_STAGE_TABLE: List[Tuple[float, str]] = [
|
||||
class WPSFortuneSystem(WPSAPI):
|
||||
"""基于整点哈希的运势系统,可供其他模块复用"""
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "提供整点运势值及阶段信息的公共组件"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "fortune",
|
||||
"identifier": "fortune",
|
||||
"description": "查询当前整点的运势值与阶段文本。",
|
||||
"metadata": {"别名": "运势"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "运势算法",
|
||||
"description": "基于用户ID与整点时间的 SHA-256 哈希映射为 [-0.9999, 0.9999] 区间。",
|
||||
},
|
||||
{
|
||||
"title": "阶段划分",
|
||||
"description": "通过 `_FORTUNE_STAGE_TABLE` 匹配阶段标签,供冒险等系统引用。",
|
||||
},
|
||||
{
|
||||
"title": "复用接口",
|
||||
"description": "`get_fortune_value / get_fortune_info` 可被其他插件同步或异步调用。",
|
||||
},
|
||||
)
|
||||
|
||||
@override
|
||||
def dependencies(self) -> List[Type]:
|
||||
return [WPSAPI]
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import List, Optional, Type
|
||||
from typing import Any, Dict, List, Optional, Sequence, Type, Union
|
||||
|
||||
from PWF.Convention.Runtime.Architecture import Architecture
|
||||
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
|
||||
from PWF.CoreModules.plugin_interface import DatabaseModel
|
||||
|
||||
from Plugins.WPSAPI import WPSAPI
|
||||
from Plugins.WPSAPI import GuideEntry, GuideSection, WPSAPI
|
||||
from Plugins.WPSBackpackSystem import (
|
||||
BackpackItemTier,
|
||||
WPSBackpackSystem,
|
||||
@@ -33,6 +33,166 @@ class WPSGardenBase(WPSAPI):
|
||||
_service: GardenService | None = None
|
||||
_initialized: bool = False
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "菜园作物种植与联动系统的核心服务"
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
service = self.service()
|
||||
config = service.config
|
||||
return {
|
||||
"作物数量": str(len(GARDEN_CROPS)),
|
||||
"最大地块": str(config.max_plots),
|
||||
"出售倍率": str(config.sale_multiplier),
|
||||
}
|
||||
|
||||
def collect_item_entries(self) -> Sequence[GuideEntry]:
|
||||
tier_counter: Dict[str, int] = {}
|
||||
wine_counter: int = 0
|
||||
for crop in GARDEN_CROPS.values():
|
||||
tier_counter[crop.tier] = tier_counter.get(crop.tier, 0) + 1
|
||||
if crop.wine_item_id:
|
||||
wine_counter += 1
|
||||
entries: List[GuideEntry] = []
|
||||
for tier, count in sorted(tier_counter.items()):
|
||||
entries.append(
|
||||
{
|
||||
"title": f"{tier.title()} 作物",
|
||||
"description": f"{count} 种作物,可收获果实与额外奖励。",
|
||||
}
|
||||
)
|
||||
entries.append(
|
||||
{
|
||||
"title": "果酒配方",
|
||||
"description": f"{wine_counter} 种作物支持果酒配方,并与战斗系统的果酒增益联动。",
|
||||
}
|
||||
)
|
||||
if GARDEN_MISC_ITEMS:
|
||||
entries.append(
|
||||
{
|
||||
"title": "杂项素材",
|
||||
"description": f"{len(GARDEN_MISC_ITEMS)} 种额外素材,可用于任务或商店出售。",
|
||||
}
|
||||
)
|
||||
return tuple(entries)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
service = self.service()
|
||||
return (
|
||||
{
|
||||
"title": "成长流程",
|
||||
"description": (
|
||||
"种植后根据作物 `growth_minutes` 决定成熟时间,系统会在成熟时通过时钟任务提醒。"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "收获收益",
|
||||
"description": (
|
||||
"收获基础产量由 `base_yield` 决定,额外奖励受运势与 `extra_reward` 配置影响。"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "果实售出",
|
||||
"description": (
|
||||
f"通过 `菜园 售出` 指令以 {service.config.sale_multiplier} 倍种子价格出售果实并获取积分。"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
def collect_additional_sections(self) -> Sequence[GuideSection]:
|
||||
sections = list(super().collect_additional_sections())
|
||||
|
||||
crop_entries: List[GuideEntry] = []
|
||||
tier_icon = {
|
||||
"common": "🌱",
|
||||
"rare": "🌳",
|
||||
"epic": "🍷",
|
||||
"legendary": "🏵️",
|
||||
}
|
||||
for crop in GARDEN_CROPS.values():
|
||||
reward_desc = ""
|
||||
if crop.extra_reward.kind == "points":
|
||||
payload = crop.extra_reward.payload
|
||||
reward_desc = (
|
||||
f"额外积分 {payload.get('min', 0)}~{payload.get('max', 0)},"
|
||||
f"触发率 {crop.extra_reward.base_rate*100:.0f}%"
|
||||
)
|
||||
elif crop.extra_reward.kind == "item":
|
||||
payload = crop.extra_reward.payload
|
||||
reward_desc = (
|
||||
f"额外物品 `{crop.extra_item_id}` 数量 {payload.get('min', 0)}~{payload.get('max', 0)},"
|
||||
f"触发率 {crop.extra_reward.base_rate*100:.0f}%"
|
||||
)
|
||||
details: List[Union[str, Dict[str, Any]]] = [
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
f"生长时间:{crop.growth_minutes} 分钟",
|
||||
f"基础产量:{crop.base_yield} 个果实",
|
||||
reward_desc or "无额外奖励",
|
||||
f"种子售价:{crop.seed_price} 分",
|
||||
],
|
||||
}
|
||||
]
|
||||
if crop.wine_item_id:
|
||||
details.append(
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
f"果酒:{crop.wine_item_id}(稀有度 {crop.wine_tier or 'rare'})",
|
||||
"炼金配方:三份果实 + 炼金坩埚 → 果酒。",
|
||||
],
|
||||
}
|
||||
)
|
||||
crop_entries.append(
|
||||
GuideEntry(
|
||||
title=crop.display_name,
|
||||
identifier=crop.seed_id,
|
||||
description=f"果实 ID:{crop.fruit_id}",
|
||||
category="作物",
|
||||
metadata={
|
||||
"稀有度": crop.tier,
|
||||
"果实ID": crop.fruit_id,
|
||||
},
|
||||
icon=tier_icon.get(crop.tier.lower(), "🌿"),
|
||||
tags=(crop.tier.title(),),
|
||||
details=details,
|
||||
)
|
||||
)
|
||||
if crop_entries:
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="作物图鉴",
|
||||
entries=crop_entries,
|
||||
layout="grid",
|
||||
section_id="garden-crops",
|
||||
description="每种作物的成长周期、产出与额外奖励说明。",
|
||||
)
|
||||
)
|
||||
|
||||
if GARDEN_MISC_ITEMS:
|
||||
misc_entries: List[GuideEntry] = []
|
||||
for item_id, meta in GARDEN_MISC_ITEMS.items():
|
||||
misc_entries.append(
|
||||
GuideEntry(
|
||||
title=meta.get("name", item_id),
|
||||
identifier=item_id,
|
||||
description=meta.get("description", ""),
|
||||
category="杂项素材",
|
||||
icon="🧺",
|
||||
)
|
||||
)
|
||||
sections.append(
|
||||
GuideSection(
|
||||
title="杂项素材",
|
||||
entries=misc_entries,
|
||||
layout="grid",
|
||||
section_id="garden-misc",
|
||||
description="园艺相关的任务或合成所需的特殊素材。",
|
||||
)
|
||||
)
|
||||
|
||||
return tuple(sections)
|
||||
|
||||
@classmethod
|
||||
def service(cls) -> GardenService:
|
||||
if cls._service is None:
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.Architecture import Architecture
|
||||
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from Plugins.WPSBackpackSystem import WPSBackpackSystem
|
||||
from Plugins.WPSConfigSystem import WPSConfigAPI
|
||||
from Plugins.WPSFortuneSystem import WPSFortuneSystem
|
||||
@@ -14,6 +15,42 @@ from .garden_plugin_base import WPSGardenBase
|
||||
|
||||
|
||||
class WPSGardenHarvest(WPSGardenBase):
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "收获成熟作物并处理额外奖励"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="收获",
|
||||
identifier="收获 <地块序号>",
|
||||
description="从成熟地块采摘果实并发放额外奖励。",
|
||||
metadata={"别名": "harvest"},
|
||||
icon="🧺",
|
||||
details=[
|
||||
{
|
||||
"type": "steps",
|
||||
"items": [
|
||||
"输入正整数地块序号。",
|
||||
"系统校验成熟状态,计算基础果实数量。",
|
||||
"发放额外奖励:积分或额外物品会自动结算。",
|
||||
],
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "指令格式",
|
||||
"description": "`收获 <地块序号>`,序号需为正整数。",
|
||||
},
|
||||
{
|
||||
"title": "收益构成",
|
||||
"description": "基础果实直接入背包,额外奖励可能为积分或额外物品。",
|
||||
},
|
||||
)
|
||||
|
||||
def wake_up(self) -> None:
|
||||
super().wake_up()
|
||||
self.register_plugin("harvest")
|
||||
|
||||
@@ -2,16 +2,54 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.Architecture import Architecture
|
||||
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from Plugins.WPSBackpackSystem import WPSBackpackSystem
|
||||
|
||||
from .garden_plugin_base import WPSGardenBase
|
||||
|
||||
|
||||
class WPSGardenPlant(WPSGardenBase):
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "种植作物并分配地块"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="种植",
|
||||
identifier="种植 <种子> [地块序号]",
|
||||
description="在指定地块种下一颗种子,默认选取下一个空地。",
|
||||
metadata={"别名": "plant"},
|
||||
icon="🌱",
|
||||
details=[
|
||||
{
|
||||
"type": "steps",
|
||||
"items": [
|
||||
"确认背包中持有对应种子。",
|
||||
"可选:指定地块序号(正整数),否则使用下一个空地。",
|
||||
"系统校验库存并写入菜园数据库,生成成熟计时。",
|
||||
],
|
||||
},
|
||||
"种植成功后立即扣除种子数量并返回成熟时间。",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "指令格式",
|
||||
"description": "`种植 <种子ID|名称> [地块序号]`,默认选择下一个空地。",
|
||||
},
|
||||
{
|
||||
"title": "库存校验",
|
||||
"description": "会检查背包种子数量,不足时返回提示而不消耗资源。",
|
||||
},
|
||||
)
|
||||
|
||||
def wake_up(self) -> None:
|
||||
super().wake_up()
|
||||
self.register_plugin("plant")
|
||||
|
||||
@@ -2,12 +2,44 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from .garden_plugin_base import WPSGardenBase
|
||||
|
||||
|
||||
class WPSGardenRemove(WPSGardenBase):
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "清理地块以重新种植"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="铲除",
|
||||
identifier="铲除 <地块序号>",
|
||||
description="清空指定地块,移除作物与成熟计时。",
|
||||
metadata={"别名": "remove"},
|
||||
icon="🧹",
|
||||
details=[
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"常用于处理枯萎或不再需要的作物。",
|
||||
"操作不可逆,被铲除的作物不会返还种子。",
|
||||
],
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "使用场景",
|
||||
"description": "用于清理枯萎或不再需要的作物,释放地块。",
|
||||
},
|
||||
)
|
||||
|
||||
def wake_up(self) -> None:
|
||||
super().wake_up()
|
||||
self.register_plugin("remove")
|
||||
|
||||
@@ -2,17 +2,55 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.Architecture import Architecture
|
||||
|
||||
from PWF.CoreModules.database import get_db
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from Plugins.WPSBackpackSystem import WPSBackpackSystem
|
||||
|
||||
from .garden_plugin_base import WPSGardenBase
|
||||
|
||||
|
||||
class WPSGardenSteal(WPSGardenBase):
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "偷取其他用户成熟果实的互动指令"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="偷取",
|
||||
identifier="偷取 <用户> <地块序号>",
|
||||
description="从其他用户的成熟作物中偷取果实。",
|
||||
metadata={"别名": "steal"},
|
||||
icon="🕵️",
|
||||
details=[
|
||||
{
|
||||
"type": "steps",
|
||||
"items": [
|
||||
"输入目标用户 ID 或昵称,以及成熟地块序号。",
|
||||
"系统校验目标用户菜园与成熟状态。",
|
||||
"成功后获得 1 个果实并通知对方被偷记录。",
|
||||
],
|
||||
},
|
||||
"同一地块可多次被不同用户偷取,超限后将提示果实不足。",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "指令格式",
|
||||
"description": "`偷取 <用户ID|昵称> <地块序号>`,不可针对自己。",
|
||||
},
|
||||
{
|
||||
"title": "通知机制",
|
||||
"description": "成功偷取后会向目标用户推送警报消息,包含剩余果实数量。",
|
||||
},
|
||||
)
|
||||
|
||||
def wake_up(self) -> None:
|
||||
super().wake_up()
|
||||
self.register_plugin("steal")
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from PWF.Convention.Runtime.Architecture import Architecture
|
||||
|
||||
from Plugins.WPSAPI import GuideEntry
|
||||
from Plugins.WPSBackpackSystem import WPSBackpackSystem
|
||||
from Plugins.WPSConfigSystem import WPSConfigAPI
|
||||
|
||||
@@ -13,6 +14,58 @@ from .garden_plugin_base import WPSGardenBase
|
||||
|
||||
|
||||
class WPSGardenView(WPSGardenBase):
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "查看菜园概览与果实售出"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
GuideEntry(
|
||||
title="菜园 概览",
|
||||
identifier="菜园",
|
||||
description="显示当前所有地块状态与剩余果实。",
|
||||
metadata={"别名": "garden"},
|
||||
icon="🗺️",
|
||||
details=[
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"列出地块序号、作物名称、成熟时间与被偷情况。",
|
||||
"空地块会提示剩余可用数量。",
|
||||
],
|
||||
}
|
||||
],
|
||||
),
|
||||
GuideEntry(
|
||||
title="菜园 售出",
|
||||
identifier="菜园 售出 <果实> <数量>",
|
||||
description="售出成熟果实并换取积分。",
|
||||
metadata={"别名": "garden sell"},
|
||||
icon="💰",
|
||||
details=[
|
||||
{
|
||||
"type": "steps",
|
||||
"items": [
|
||||
"检查背包持有的果实数量。",
|
||||
"输入欲出售的果实 ID/名称与数量。",
|
||||
"系统按配置的售价乘以数量发放积分,同时扣除背包库存。",
|
||||
],
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "概览视图",
|
||||
"description": "默认输出地块编号、成熟状态、剩余果实与被偷记录。",
|
||||
},
|
||||
{
|
||||
"title": "果实售出",
|
||||
"description": "`菜园 售出 <果实> <数量>`,自动结算积分并扣除背包库存。",
|
||||
},
|
||||
)
|
||||
|
||||
def wake_up(self) -> None:
|
||||
super().wake_up()
|
||||
self.register_plugin("garden")
|
||||
|
||||
@@ -83,6 +83,42 @@ class WPSStoreSystem(WPSAPI):
|
||||
logger.SaveProperties()
|
||||
self._permanent_mode_ids: set[str] = set()
|
||||
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "系统商品与玩家寄售的统一商店"
|
||||
|
||||
def get_guide_metadata(self) -> Dict[str, str]:
|
||||
return {
|
||||
"已注册模式": str(len(self._mode_registry)),
|
||||
"永久模式": str(len(self._permanent_mode_ids)),
|
||||
"数据表": f"{self.SYSTEM_TABLE}, {self.PLAYER_TABLE}",
|
||||
}
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "store",
|
||||
"identifier": "store",
|
||||
"description": "查看系统刷新的商品列表,包含系统和玩家寄售。",
|
||||
"metadata": {"别名": "商店"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "模式注册",
|
||||
"description": "`register_mode/register_permanent_mode` 将背包物品以指定库存与价格投放至系统表。",
|
||||
},
|
||||
{
|
||||
"title": "刷新机制",
|
||||
"description": "每小时根据 `store_hourly_count` 配置刷新系统库存,同时同步永久模式。",
|
||||
},
|
||||
{
|
||||
"title": "玩家寄售",
|
||||
"description": "`sell_item` 将玩家物品挂售至寄售表,`purchase_item` 支持购买系统或玩家商品。",
|
||||
},
|
||||
)
|
||||
|
||||
@override
|
||||
def dependencies(self) -> List[type]:
|
||||
return [WPSAPI, WPSConfigAPI, WPSBackpackSystem]
|
||||
@@ -960,6 +996,31 @@ class WPSStoreSystem(WPSAPI):
|
||||
|
||||
|
||||
class WPSStoreBuyCommand(WPSAPI):
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "购买商店及玩家寄售物品"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "buy",
|
||||
"identifier": "buy",
|
||||
"description": "购买系统或玩家寄售商品,数量需为正整数。",
|
||||
"metadata": {"别名": "购买"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "参数格式",
|
||||
"description": "`购买 <物品名称或ID> <数量>`,内部支持模糊匹配模式名称。",
|
||||
},
|
||||
{
|
||||
"title": "权限校验",
|
||||
"description": "调用 `purchase_item` 时校验库存、积分并自动扣除商品库存。",
|
||||
},
|
||||
)
|
||||
|
||||
@override
|
||||
def dependencies(self) -> list[type]:
|
||||
return [WPSStoreSystem]
|
||||
@@ -1016,6 +1077,31 @@ class WPSStoreBuyCommand(WPSAPI):
|
||||
|
||||
|
||||
class WPSStoreSellCommand(WPSAPI):
|
||||
def get_guide_subtitle(self) -> str:
|
||||
return "挂售物品至商店寄售区"
|
||||
|
||||
def collect_command_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "sell",
|
||||
"identifier": "sell",
|
||||
"description": "将背包物品以指定数量和单价挂售。",
|
||||
"metadata": {"别名": "出售"},
|
||||
},
|
||||
)
|
||||
|
||||
def collect_guide_entries(self) -> Sequence[GuideEntry]:
|
||||
return (
|
||||
{
|
||||
"title": "参数格式",
|
||||
"description": "`出售 <物品名称或ID> <数量> <单价>`。",
|
||||
},
|
||||
{
|
||||
"title": "寄售生命周期",
|
||||
"description": "寄售记录写入玩家表,状态变更后定期清理无效记录。",
|
||||
},
|
||||
)
|
||||
|
||||
@override
|
||||
def dependencies(self) -> list[type]:
|
||||
return [WPSStoreSystem]
|
||||
|
||||
Reference in New Issue
Block a user