新增插件指引网页

This commit is contained in:
2025-11-12 22:58:36 +08:00
parent 4a3beb2153
commit 7332141a92
34 changed files with 2373 additions and 8984 deletions

View File

@@ -10,7 +10,7 @@ alwaysApply: true
语言设置:除非用户另有指示,所有常规交互响应都应该使用中文。然而,模式声明(例如\[MODE: RESEARCH\])和特定格式化输出(例如代码块、清单等)应保持英文,以确保格式一致性。 语言设置:除非用户另有指示,所有常规交互响应都应该使用中文。然而,模式声明(例如\[MODE: RESEARCH\])和特定格式化输出(例如代码块、清单等)应保持英文,以确保格式一致性。
工具调用: 你应该使用工具调用而不是通过命令行编辑文件 **工具调用: 你应该使用工具调用而不是通过命令行编辑文件**
你必须完全理解这个项目, 并明白文件夹PWF里的文件你没有权力更改 你必须完全理解这个项目, 并明白文件夹PWF里的文件你没有权力更改

View File

@@ -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 # 限流控制
```
## 技术栈
- FastAPIWeb框架
- 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模式完成后填写

View File

@@ -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)`作为唯一键
- 五子棋需要双人对战,需要同时记录两个玩家
- 需要设计状态数据结构,存储:
- 对战双方IDplayer1_id, player2_id
- 当前轮到谁current_player_id
- 棋盘状态15x15二维数组
- 游戏状态waiting, playing, finished
- 胜者IDwinner_id如果有
### 2. 多轮对战并发
- 允许同一个chat中有多轮对战只要对战双方不同
- 需要一个机制来标识不同的对战局可以用对战双方ID的组合
- 状态查询需要能够找到特定用户参与的对战
### 3. 禁手规则实现
禁手规则(仅对黑方,即先手玩家):
- **三三禁手**:一手棋同时形成两个或以上的活三
- **四四禁手**:一手棋同时形成两个或以上的活四或冲四
- **长连禁手**:一手棋形成六子或以上的连珠
需要实现:
- 判断某个位置的四个方向(横、竖、左斜、右斜)的连珠情况
- 判断活三、活四、冲四的定义
- 在落子时检查是否触发禁手
### 4. 坐标系统
-A-O15列
-1-1515行
- 需要坐标转换函数:`parse_coord("A1") -> (0, 0)`
- 需要显示转换函数:`format_coord(0, 0) -> "A1"`
### 5. 棋盘显示
使用emoji
- ⚫ 黑子(先手)
- ⚪ 白子(后手)
- 空位
- 需要添加行号和列号标注
示例:
```
A B C D E F G H I J K L M N O
1
2
3 ➕➕⚫➕➕➕➕➕➕➕➕➕➕➕➕
...
```
## 数据结构设计
### state_data结构
```python
{
"player1_id": 123456, # 黑方(先手)
"player2_id": 789012, # 白方(后手)
"current_player": 123456, # 当前轮到谁
"board": [[0]*15 for _ in range(15)], # 0:空, 1:黑, 2:白
"status": "playing", # waiting, playing, finished
"winner_id": None, # 胜者ID
"moves": [], # 历史落子记录 [(row, col, player_id), ...]
"last_move": None # 最后一手 (row, col)
}
```
### 游戏状态存储策略
- 使用chat_id作为会话ID
- 使用较小的user_id作为主键中的user_id保证唯一性
- 在state_data中存储完整的对战信息
- 查询时需要检查用户是否是player1或player2
# 提议的解决方案
## 方案选择
使用现有的数据库表结构通过精心设计state_data来支持双人对战。
## 实现方案
### 1. 游戏类:`games/gomoku.py`
继承`BaseGame`,实现以下方法:
- `handle()` - 主处理逻辑
- `get_help()` - 帮助信息
- `_start_game()` - 开始游戏
- `_make_move()` - 落子
- `_show_board()` - 显示棋盘
- `_resign()` - 认输
- `_get_stats()` - 查看战绩
### 2. 五子棋逻辑:单独模块或工具类
- `_parse_coord()` - 解析坐标
- `_format_coord()` - 格式化坐标
- `_render_board()` - 渲染棋盘
- `_check_win()` - 检查胜负
- `_check_forbidden()` - 检查禁手
- `_is_valid_move()` - 检查落子是否合法
### 3. 禁手检测逻辑
实现辅助方法:
- `_count_line()` - 统计某方向的连珠情况
- `_is_live_three()` - 判断活三
- `_is_live_four()` - 判断活四
- `_is_rush_four()` - 判断冲四
- `_check_three_three()` - 检查三三禁手
- `_check_four_four()` - 检查四四禁手
- `_check_overline()` - 检查长连禁手
### 4. 状态管理
- 使用`min(player1_id, player2_id)`作为数据库中的user_id
- 在state_data中完整存储对战信息
- 提供辅助方法查找用户当前参与的游戏
### 5. 路由注册
`routers/callback.py``handle_command()`函数中添加:
```python
if game_type == 'gomoku':
from games.gomoku import GomokuGame
game = GomokuGame()
return await game.handle(command, chat_id, user_id)
```
### 6. 指令解析
`utils/parser.py``CommandParser`类中添加gomoku指令识别
### 7. 配置更新
`config.py`中添加五子棋相关配置(如果需要)
# 当前执行步骤:"已完成所有实施步骤"
# 任务进度
## [2025-10-28 17:18:21]
- 已修改:
- config.py - 添加gomoku配置
- utils/parser.py - 添加gomoku指令映射
- games/gomoku_logic.py - 创建五子棋逻辑模块(新文件)
- games/gomoku.py - 创建五子棋游戏类(新文件)
- routers/callback.py - 添加gomoku路由
- games/base.py - 更新帮助信息和统计信息
- 更改:完成五子棋游戏的完整实现,包括:
- 群级游戏池管理(支持多轮对战并存)
- 标准15x15棋盘
- 完整的禁手规则(三三、四四、长连)
- 坐标系统A-O列1-15行
- emoji棋盘渲染⚫⚪
- 胜负判定
- 战绩统计
- 原因:实现用户需求的双人对战五子棋游戏
- 阻碍因素:用户识别和显示格式错误
- 状态:不成功
## [2025-10-28 17:36:07]
- 已修改:
- games/gomoku.py - 修复用户识别和显示格式
- 更改:
- 修复 `_parse_opponent()` 方法使用正确的WPS @用户格式 `<at user_id="xxx"></at>` 进行解析
- 修改所有用户显示,从 `@用户{user_id}` 改为 `<at user_id="{user_id}"></at>`,以正确显示用户的群名称
- 涉及修改:开始游戏、落子、显示棋盘、认输、列出对战等所有用户显示位置
- 原因:修复用户识别失败和显示错误的问题
- 阻碍因素:用户识别仍然失败
- 状态:不成功
## [2025-10-28 17:42:24]
- 已修改:
- games/gomoku.py - 改进用户ID解析和添加调试日志
- routers/callback.py - 增强日志输出
- 更改:
- 改进 `_parse_opponent()` 方法,支持多种@用户格式(双引号、单引号、不同的标签格式)
-`handle()` 方法中添加详细的调试日志command, args, action, opponent_id
- 改进错误提示,显示实际接收到的参数内容
- 将 callback.py 中的消息内容日志级别从 DEBUG 改为 INFO便于追踪
- 原因进一步诊断用户ID识别失败的问题添加调试信息帮助定位问题
- 阻碍因素WPS callback不提供被@用户的ID信息
- 状态:不成功
## [2025-10-28 17:55:00]
- 已修改:
- games/gomoku.py - 重构游戏发起机制,从@用户改为挑战-接受模式
- games/base.py - 更新全局帮助信息
- routers/callback.py - 添加完整callback数据日志
- 更改:
- **核心架构变更**:从"@用户发起对战"改为"挑战-接受"机制
- 新增 `_create_challenge()` 方法 - 用户发起挑战
- 新增 `_accept_challenge()` 方法 - 其他用户接受挑战
- 新增 `_cancel_challenge()` 方法 - 取消自己的挑战
- 删除 `_parse_opponent()` 方法(不再需要)
- 删除 `_start_game()` 方法(由新方法替代)
- 更新游戏池数据结构,添加 `challenges` 列表
- 更新所有帮助信息和错误提示
- 指令变更:
- `.gomoku challenge` / `.gomoku start` - 发起挑战
- `.gomoku accept` / `.gomoku join` - 接受挑战
- `.gomoku cancel` - 取消挑战
- 原因WPS callback消息内容中@用户只是文本形式(如"@揭英飙"不包含user_id无法实现@用户发起对战
- 阻碍因素:无
- 状态:成功
## [2025-10-28 17:56:03]
- 已修改:
- games/gomoku_logic.py - 修复棋盘对齐问题
- 更改:
- 优化 `render_board()` 函数的格式化逻辑
- 列标题:每个字母后面加一个空格,确保与棋子列对齐
- 行号:调整前导空格,从 " {row_num} " 改为 "{row_num} "
- 棋子每个emoji后面加一个空格行尾去除多余空格
- 整体对齐确保列标题、行号、棋子三者在Markdown代码块中正确对齐
- 原因:修复用户反馈的棋盘文本对齐问题
- 阻碍因素:无
- 状态:未确认
# 最终审查
(待完成后填写)

View File

@@ -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_idWPS用户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-10010次机会
- ✅ 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. 详细的文档和部署指南
**推荐操作**: 立即部署到生产环境开始使用

View File

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

View File

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

View File

@@ -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/ # 数据文件
```
## 技术栈
- FastAPIWeb框架
- SQLite数据存储
- llama-index-coreAI对话框架核心
- llama-index-llms-ollamaOllama LLM集成
- Ollama本地LLM服务
# 分析
## 现有架构
1. **指令处理流程**
- 消息通过 `/api/callback` 接收
- `CommandParser` 解析指令,只处理以 `.` 开头的命令
- 非指令消息会被忽略
- 指令分发到对应的游戏处理器
2. **状态管理**
- 游戏状态存储在 `game_states`
- 使用 `(chat_id, user_id, game_type)` 作为联合主键
- 对于群组共享状态,使用 `user_id=0`(如成语接龙)
3. **异步任务**
- 已有 `periodic_cleanup` 后台清理任务示例
- 使用 `asyncio.create_task``asyncio.sleep` 实现
## 关键技术挑战
### 1. 延迟回答机制
需要实现一个基于时间间隔的判断机制:
- 收到 `.ai` 指令时,将消息加入等待队列
- 设置一个等待窗口例如5-10秒
- 如果在等待窗口内有新消息,重新计时
- 等待窗口结束后,如果没有新消息,生成回答
- 需要在 `chat_id` 级别维护等待队列和延迟任务
### 2. 长上下文管理
- 使用 llama_index 的 `ChatMemoryBuffer` 管理对话历史
- 确保超过30轮对话历史能够被保留
- 对话历史需要按 `chat_id` 独立存储
- 对话历史中需要包含用户ID信息以便区分不同用户
### 3. Ollama配置管理
- 使用全局单一配置(服务器级别)
- 配置存储在 `data/ai_config.json` 文件中
- 配置包括:服务地址、端口、模型名称
- 通过 `.aiconfig` 指令修改配置并持久化到文件
- 配置需要有默认值localhost:11434默认模型需指定
### 4. 多用户对话识别
- 对话历史中需要记录每条消息的发送者user_id
- 生成回复时,需要识别上下文中的不同用户
- 回复格式可以考虑使用 @用户 的方式
### 5. 依赖管理
- 需要添加 llama-index-core 和相关依赖
- 需要确保与现有代码库的兼容性
- 考虑资源占用内存、CPU
## 数据结构设计
### AI对话状态数据结构
对话状态由 llama_index 的 ChatMemoryBuffer 管理,存储在内存中。
需要存储的额外信息:
```python
# 存储在 game_states 表中的 state_data
{
"user_mapping": { # 用户ID到角色名称的映射
"123456": "用户1",
"789012": "用户2",
...
},
"user_count": 2 # 当前对话中的用户数量
}
```
### 配置数据结构(存储在 data/ai_config.json
```json
{
"host": "localhost",
"port": 11434,
"model": "llama3.1"
}
```
### 数据库扩展
使用 `game_states` 表存储用户映射信息:
- `chat_id`: 会话ID
- `user_id`: 0表示群组级别
- `game_type`: "ai_chat"
- `state_data`: JSON格式的用户映射信息
注意:对话历史由 ChatMemoryBuffer 在内存中管理,不持久化到数据库。
# 提议的解决方案
## 方案概述
1. 创建一个新的游戏模块 `games/ai_chat.py`,继承 `BaseGame`
2. 使用 `game_states` 表存储用户映射信息用户ID到角色名称的映射
3. 使用全局字典维护每个 `chat_id` 的延迟任务句柄
4. 使用全局字典维护每个 `chat_id` 的 ChatEngine 实例和待处理消息队列
5. 使用 `data/ai_config.json` 存储 Ollama 全局配置
6. 使用 llama_index 的 ChatMemoryBuffer 管理对话上下文(内存中)
## 实现细节
### 1. 指令注册
`utils/parser.py` 中添加:
- `.ai`: 触发AI对话
- `.aiconfig`: 配置Ollama参数
### 2. AI对话模块 (`games/ai_chat.py`)
- `handle()`: 主处理函数,处理 `.ai``.aiconfig` 指令
- `_handle_ai()`: 处理AI对话请求
- 将消息加入等待队列
- 取消旧的延迟任务(如果存在)
- 创建新的延迟任务10秒后执行
- `_handle_config()`: 处理配置请求
- 解析配置参数host, port, model
- 更新 `data/ai_config.json` 文件
- 返回配置确认消息
- `_add_to_queue()`: 将消息加入等待队列(按 chat_id 组织)
- `_delayed_response()`: 延迟回答任务(内部异步函数)
- 等待10秒后执行
- 检查队列并生成回答
- 处理任务取消异常
- `_generate_response()`: 使用LLM生成回答
- 获取或创建 ChatEngine 实例
- 获取用户角色映射
- 将队列中的消息按用户角色格式化
- 调用 ChatEngine.chat() 生成回答
- 更新 ChatMemoryBuffer
- `_get_chat_engine()`: 获取或创建ChatEngine实例
- 检查全局字典中是否已存在
- 不存在则创建新的 ChatEngine配置 ChatMemoryBuffer
- 设置系统提示(告知多用户场景)
- `_get_user_role()`: 获取用户角色名称(创建或获取映射)
- `_load_config()`: 从 JSON 文件加载配置
- `_save_config()`: 保存配置到 JSON 文件
### 3. 延迟任务管理
- 使用全局字典 `_pending_tasks` 存储每个 `chat_id` 的延迟任务句柄
- 使用全局字典 `_message_queues` 存储每个 `chat_id` 的待处理消息队列
- 使用全局字典 `_chat_engines` 存储每个 `chat_id` 的 ChatEngine 实例
- 新消息到达时,取消旧任务(调用 task.cancel())并创建新任务
- 使用 `asyncio.create_task``asyncio.sleep(10)` 实现固定10秒延迟
- 处理 `asyncio.CancelledError` 异常,避免任务取消时的错误日志
### 4. 用户角色映射机制
- 为每个 chat_id 维护用户ID到角色名称的映射如"用户1"、"用户2"
- 映射信息存储在 `game_states` 表中chat_id, user_id=0, game_type='ai_chat'
- 首次出现的用户自动分配角色名称(按出现顺序)
- 在将消息添加到 ChatMemoryBuffer 时使用角色名称作为消息角色
- 系统提示中包含:"这是一个多用户对话场景,不同用户的发言会用不同的角色标识。你需要理解不同用户的发言内容,并根据上下文给出合适的回复。"
### 4. 依赖添加
`requirements.txt` 中添加:
```
llama-index-core>=0.10.0
llama-index-llms-ollama>=0.1.0
```
### 5. 路由注册
`routers/callback.py``handle_command()` 中添加AI对话处理分支
### 6. 帮助信息更新
`games/base.py``get_help_message()` 中添加AI对话帮助
## 时间间隔判断逻辑固定10秒窗口
1. **默认等待窗口**10秒固定
2. **收到 `.ai` 指令时**
- 提取消息内容(去除 `.ai` 前缀)
- 获取用户ID和chat_id
- 将消息用户ID + 内容)加入该 `chat_id` 的等待队列
- 如果有待处理的延迟任务(检查 `_pending_tasks[chat_id]`),取消它
- 创建新的延迟任务(`asyncio.create_task(_delayed_response(chat_id))`
- 将任务句柄存储到 `_pending_tasks[chat_id]`
3. **在等待窗口内收到新消息**(无论是否是指令):
- 如果新消息也是 `.ai` 指令:
- 将新消息加入队列
- 取消当前延迟任务(`task.cancel()`
- 创建新的延迟任务重新计时10秒
- 如果新消息不是指令但chat_id在等待队列中
- 可以考虑忽略,或也加入队列(根据需求决定)
4. **等待窗口结束(延迟任务执行)**
- 检查队列中是否有消息
- 如果有,获取该 chat_id 的 ChatEngine 和用户映射
- 将队列中的消息按用户角色格式化后添加到 ChatMemoryBuffer
- 调用 ChatEngine.chat() 生成回答
- 清空队列
-`_pending_tasks` 中移除任务句柄
## 配置文件管理data/ai_config.json
- 文件结构:
```json
{
"host": "localhost",
"port": 11434,
"model": "llama3.1"
}
```
- 首次加载时如果文件不存在,创建默认配置
- 通过 `.aiconfig` 指令修改配置时,实时保存到文件
- ChatEngine 创建时从配置文件加载配置
# 当前执行步骤:"4. 执行模式 - 代码实施完成并测试通过"
# 任务进度
## [2025-10-29_23:55:08] 执行阶段完成
- 已修改:
- requirements.txt添加 llama-index-core 和 llama-index-llms-ollama 依赖
- data/ai_config.json创建默认配置文件
- utils/parser.py添加 .ai 和 .aiconfig 指令映射和解析逻辑
- games/ai_chat.py创建完整的 AI 对话模块实现
- routers/callback.py添加 ai_chat 处理分支
- games/base.py添加 AI 对话帮助信息
- 更改:
- 实现了基于 llama_index 和 Ollama 的 AI 对话功能
- 实现了固定10秒等待窗口的延迟回答机制
- 实现了用户角色映射和长上下文管理
- 实现了配置文件的 JSON 存储和管理
- 原因:按照计划实施 AI 对话功能的所有核心组件
- 阻碍因素:无
- 状态:成功
## [2025-10-30_00:56:44] 功能优化和问题修复
- 已修改:
- games/ai_chat.py优化错误处理和用户体验
1. 移除收到消息后的确认回复(静默处理)
2. 修复转义字符警告SyntaxWarning
3. 改进错误处理,提供详细的调试信息和排查步骤
4. 添加超时设置120秒
5. 针对NPS端口转发的特殊错误提示
- 更改:
- 优化了错误提示信息,包含当前配置、测试命令和详细排查步骤
- 专门针对NPS端口转发场景添加了Ollama监听地址配置说明
- 改进了连接错误的诊断能力
- 原因:根据实际使用中发现的问题进行优化
- 阻碍因素:无
- 状态:成功
## [2025-10-30_01:10:05] 系统提示词持久化和功能完善
- 已修改:
- games/ai_chat.py
1. 实现系统提示词的持久化存储(保存到配置文件)
2. 添加 `_get_default_system_prompt()` 方法定义默认系统提示词
3. 添加 `_get_system_prompt()` 方法从配置文件加载系统提示词
4. 更新系统提示词内容明确AI身份和职责
5. 在系统提示词中包含完整的机器人功能列表和指引
- 更改:
- 系统提示词现在会保存到 `data/ai_config.json` 文件中
- 服务重启后系统提示词会自动从配置文件加载,保持长期记忆
- AI助手能够了解自己的身份和所有机器人功能可以主动指引用户
- 系统提示词包含了完整的13个功能模块介绍和回复指南
- 原因实现系统提示词的长期记忆让AI能够始终记住自己的身份和职责
- 阻碍因素:无
- 状态:成功
# 最终审查
## 实施总结
✅ 所有计划功能已成功实施并通过测试
### 核心功能实现
1. ✅ AI对话系统基于 llama_index + Ollama 构建
2. ✅ 显式指令触发(`.ai <问题>`
3. ✅ 配置指令(`.aiconfig`支持动态配置Ollama服务
4. ✅ 固定10秒等待窗口的延迟回答机制
5. ✅ 用户角色映射和长上下文管理30+轮对话)
6. ✅ 配置文件持久化存储
7. ✅ 系统提示词持久化存储(新增)
8. ✅ 完善的错误处理和调试信息
### 文件修改清单
- ✅ requirements.txt - 添加依赖
- ✅ data/ai_config.json - 配置文件(包含系统提示词)
- ✅ utils/parser.py - 指令解析
- ✅ games/ai_chat.py - AI对话模块完整实现
- ✅ routers/callback.py - 路由注册
- ✅ games/base.py - 帮助信息更新
### 技术特性
- ✅ 多用户对话支持
- ✅ 延迟任务管理asyncio
- ✅ ChatMemoryBuffer长上下文管理
- ✅ JSON配置文件管理
- ✅ NPS端口转发支持
- ✅ 详细的错误诊断和排查指南
### 测试状态
- ✅ 功能测试通过
- ✅ Ollama服务连接测试通过
- ✅ NPS端口转发配置测试通过
- ✅ 系统提示词持久化测试通过
## 实施与计划匹配度
实施与计划完全匹配 ✅
## 补充分析Markdown 渲染与发送通道2025-10-30
### 现状观察
- `routers/callback.py` 中仅当 `response_text.startswith('#')` 时才通过 `send_markdown()` 发送,否则使用 `send_text()`。这意味着即使 AI 返回了合法的 Markdown但不以 `#` 开头例如代码块、列表、表格、普通段落等也会被按纯文本通道发送导致下游WPS 侧)不进行 Markdown 渲染。
- `games/ai_chat.py` 的 `_generate_response()` 直接返回 `str(response)`,未对内容类型进行标注或判定,上层仅依赖首字符为 `#` 的启发式判断来选择发送通道。
- `utils/message.py` 已具备 `send_markdown()` 与 `send_text()` 两种发送方式,对应 `{"msgtype":"markdown"}` 与 `{"msgtype":"text"}` 消息结构;当前缺少自动识别 Markdown 的逻辑。
### 影响
- 当 AI 返回包含 Markdown 元素但非标题(不以 `#` 开头)的内容时,用户端看到的是未渲染的原始 Markdown 文本,表现为“格式不能成功排版”。
### 待确认问题(不含解决方案,需产品/实现口径)
1. 目标平台WPS 机器人)对 Markdown 的要求是否仅需 `msgtype=markdown` 即可渲染?是否存在必须以标题开头的限制?
2. 期望策略:
- 是否希望“.ai 的所有回复”统一走 Markdown 通道?
- 还是需要基于 Markdown 特征进行判定(如代码块、列表、链接、表格、行内格式等)?
3. 兼容性:若统一改为 Markdown 通道,是否会影响既有纯文本展示(例如换行、转义、表情)?
4. 其他指令模块是否也可能返回 Markdown若有是否一并纳入同一策略
### 相关代码参照点(路径)
- `routers/callback.py`:回复通道选择逻辑(基于 `startswith('#')`
- `games/ai_chat.py`AI 回复内容生成与返回(直接返回字符串)
- `utils/message.py``send_markdown()` 与 `send_text()` 的消息结构
### 决策结论与范围2025-10-30
- 分支策略:不创建新分支,继续在当前任务上下文内推进。
- 发送策略:`.ai` 产生的回复统一按 Markdown 发送。
- 影响范围:仅限 AI 对话功能(`.ai`/`ai_chat`),不扩展到其他指令模块。
# 任务进度(补充)
## [2025-10-30_??:??:??] 标注 Markdown 渲染问题(记录现状与待确认项)
- 已修改:
- `.tasks/2025-10-29_3_ai_chat.md`补充“Markdown 渲染与发送通道”分析与待确认清单(仅问题陈述,无解决方案)。
- 更改:
- 明确当前仅以标题开头触发 Markdown 发送的启发式导致部分 Markdown 未被渲染。
- 原因:
- 用户反馈“AI 返回内容支持 Markdown但当前直接当作文本返回导致无法正确排版”。
- 阻碍因素:
- 目标平台的 Markdown 渲染细节与统一策略选择待确认。
- 状态:
- 未确认(等待策略口径与平台渲染规范确认)。
## [2025-10-30_11:40:31] 执行AI 回复统一按 Markdown 发送(仅限 AI
- 已修改:
- `routers/callback.py`:在 `callback_receive()` 的发送阶段,当 `game_type == 'ai_chat'` 且存在 `response_text` 时,无条件调用 `send_markdown(response_text)`;若发送异常,记录日志并回退到 `send_text(response_text)`;其他指令模块继续沿用 `startswith('#')` 的启发式逻辑。
- 更改:
- 使 `.ai` 产生的回复在 WPS 端稳定触发 Markdown 渲染,不再依赖以 `#` 开头。
- 原因:
- 对齐“统一按 Markdown 发送(仅限 AI”的决策解决 Markdown 文本被当作纯文本发送导致的排版问题。
- 阻碍因素:
- 暂无。
- 状态:
- 成功。

View File

@@ -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纯数据库表方案推荐
**优点**
- 数据结构清晰,便于查询统计
- 支持历史记录追踪
- 并发安全,利用数据库事务
- 易于扩展复杂查询
**缺点**
- 需要维护额外的表结构
- 稍微复杂一些
**决策**:采用此方案
### 方案Bgame_states + JSON方案
**优点**
- 复用现有系统
- 实现简单
**缺点**
- 难以进行复杂统计查询
- JSON解析性能较差
- 数据格式不够规范化
## 核心实现细节
### 1. 游戏流程控制
- **开启游戏**检查是否已有活跃游戏同一chat_id只能有一个进行中的游戏
- **下注限制**检查session状态、下注金额范围、玩家积分
- **结算控制**只有庄家可以结算结算后自动关闭session
### 2. 下注流程
1. 检查是否有活跃的session
2. 检查下注金额是否符合min/max限制
3. 检查用户积分是否充足
4. 扣除下注金额consume_points
5. 记录下注到casino_bets表
### 3. 结算流程
1. 验证是否为庄家操作
2. 查询所有pending状态的下注
3. 计算每个玩家的输赢
4. 使用数据库事务确保原子性:
- 更新bets状态
- 发放/扣除积分
- 更新session状态
5. 返回结算报告
### 4. 抽水机制
- **抽水率**5%可配置存储在session.house_fee中
- **抽水时机**:从玩家的赢得金额中扣除
- **抽水归属**:归系统所有(不返还给庄家)
- **计算方式**
- 玩家赢得 = 下注金额 × 赔率
- 实际发放 = 赢得金额 × (1 - 抽水率)
- 抽水金额 = 赢得金额 × 抽水率
### 5. 错误处理
- 下注时积分不足:给出明确提示
- 重复下注:允许(可下多注)
- 非法下注金额:给出范围提示
- 非庄家尝试结算:拒绝
## 安全性
- 下注前检查积分
- 结算时使用数据库事务保证原子性
- 抽水机制保护庄家(虽然抽水归系统)
- 验证庄家身份
- 防止重复结算
# 详细实施计划
## 文件1: core/database.py
### 修改函数: init_tables()
在现有表创建之后约第130行添加赌场相关表的创建
位置:在 `user_points` 表创建之后约第130行添加
```python
# 赌场下注记录表
cursor.execute("""
CREATE TABLE IF NOT EXISTS casino_bets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
game_type TEXT NOT NULL,
user_id INTEGER NOT NULL,
bet_type TEXT NOT NULL,
amount INTEGER NOT NULL,
multiplier REAL NOT NULL,
status TEXT DEFAULT 'pending',
result TEXT,
win_amount INTEGER,
created_at INTEGER NOT NULL,
settled_at INTEGER,
FOREIGN KEY (user_id) REFERENCES users (user_id)
)
""")
# 赌场游戏会话表
cursor.execute("""
CREATE TABLE IF NOT EXISTS casino_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
game_type TEXT NOT NULL,
banker_id INTEGER NOT NULL,
min_bet INTEGER NOT NULL,
max_bet INTEGER NOT NULL,
multiplier REAL NOT NULL,
house_fee REAL DEFAULT 0.05,
status TEXT DEFAULT 'open',
created_at INTEGER NOT NULL,
settled_at INTEGER,
UNIQUE(chat_id, game_type, status)
)
""")
# 创建索引
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_casino_bets
ON casino_bets(chat_id, game_type, status)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_casino_sessions
ON casino_sessions(chat_id, game_type, status)
""")
```
### 新增函数: create_casino_session()
函数签名:
```python
def create_casino_session(self, chat_id: int, game_type: str, banker_id: int,
min_bet: int, max_bet: int, multiplier: float,
house_fee: float = 0.05) -> int:
```
功能创建新的赌场游戏会话返回session_id
### 新增函数: get_active_casino_session()
函数签名:
```python
def get_active_casino_session(self, chat_id: int, game_type: str) -> Optional[Dict]:
```
功能:获取活跃的游戏会话
### 新增函数: create_casino_bet()
函数签名:
```python
def create_casino_bet(self, chat_id: int, game_type: str, user_id: int,
bet_type: str, amount: int, multiplier: float) -> int:
```
功能创建下注记录返回bet_id
### 新增函数: get_pending_bets()
函数签名:
```python
def get_pending_bets(self, chat_id: int, game_type: str) -> List[Dict]:
```
功能:获取待结算的下注列表
### 新增函数: settle_casino_bets()
函数签名:
```python
def settle_casino_bets(self, chat_id: int, game_type: str, result: str,
banker_id: int) -> Dict:
```
功能结算所有下注返回结算详情字典winners, losers, total_win等
### 新增函数: close_casino_session()
函数签名:
```python
def close_casino_session(self, chat_id: int, game_type: str):
```
功能:关闭游戏会话
## 文件2: games/casino.py新建
### 类: CasinoGame
继承自 `BaseGame`
### 方法: __init__()
初始化数据库连接
### 方法: async handle(command, chat_id, user_id) -> str
主处理函数,解析指令并调用相应的处理方法
解析逻辑:
- 提取命令参数,格式:`.赌场 <游戏类型> <操作> <参数>`
- 识别游戏类型(第一期只支持"大小"
- 分发到相应的处理方法
### 方法: async _handle_bigsmall(command, args, chat_id, user_id) -> str
处理大小游戏的各种操作
支持的操作:
- open: 开启游戏
- bet: 下注
- status: 查看状态
- settle: 结算
### 方法: async _open_bigsmall(args, chat_id, user_id) -> str
庄家开启大小游戏
参数解析:`<最小下注> <最大下注> <赔率>`
参数验证和限制
### 方法: async _bet_bigsmall(args, chat_id, user_id) -> str
玩家下注
参数解析:`<大小/小> <下注金额>`
检查session、金额范围、用户积分
### 方法: async _status_bigsmall(chat_id, game_type) -> str
查看当前游戏状态
### 方法: async _settle_bigsmall(args, chat_id, user_id) -> str
庄家结算游戏
参数解析:`<大/小>`
验证庄家身份,结算所有下注
### 方法: get_help() -> str
返回帮助信息
## 文件3: utils/parser.py
### 修改: COMMAND_MAP
添加赌场指令映射:
```python
# 赌场系统
'.赌场': 'casino',
'.casino': 'casino',
```
## 文件4: routers/callback.py
### 修改: async handle_command()
在AI对话系统之后约第209行添加
```python
# 赌场系统
if game_type == 'casino':
from games.casino import CasinoGame
game = CasinoGame()
return await game.handle(command, chat_id, user_id)
```
## 文件5: games/base.py
### 修改: get_help_message()
在积分赠送系统之后添加赌场游戏帮助:
```python
### 🎰 赌场系统
- `.赌场 大小 open <最小> <最大> <赔率>` - 庄家开启大小游戏
- `.赌场 大小 bet </> <金额>` - 下注
- `.赌场 大小 status` - 查看状态
- `.赌场 大小 settle </>` - 庄家结算
```
## 实施清单
1. 修改 `core/database.py``init_tables()` 方法,添加赌场表创建和索引
2.`core/database.py` 中添加 `create_casino_session()` 方法
3.`core/database.py` 中添加 `get_active_casino_session()` 方法
4.`core/database.py` 中添加 `create_casino_bet()` 方法
5.`core/database.py` 中添加 `get_pending_bets()` 方法
6.`core/database.py` 中添加 `settle_casino_bets()` 方法
7.`core/database.py` 中添加 `close_casino_session()` 方法
8. 创建文件 `games/casino.py`,定义 `CasinoGame`
9.`games/casino.py` 中实现 `__init__()` 方法
10.`games/casino.py` 中实现 `async handle()` 方法
11.`games/casino.py` 中实现 `async _handle_bigsmall()` 方法
12.`games/casino.py` 中实现 `async _open_bigsmall()` 方法
13.`games/casino.py` 中实现 `async _bet_bigsmall()` 方法
14.`games/casino.py` 中实现 `async _status_bigsmall()` 方法
15.`games/casino.py` 中实现 `async _settle_bigsmall()` 方法
16.`games/casino.py` 中实现 `get_help()` 方法
17. 修改 `utils/parser.py`,在 COMMAND_MAP 中添加赌场指令映射
18. 修改 `routers/callback.py`,在 `handle_command()` 中添加赌场路由
19. 修改 `games/base.py`,在 `get_help_message()` 中添加赌场帮助信息
20. 测试所有功能点,确保无错误
# 当前执行步骤:"2. 详细技术规划完成,等待进入实现阶段"
# 任务进度
[2025-10-30_15:16:56]
- 已修改:创建任务文件 `.tasks/2025-10-30_1_add_casino_games.md`
- 更改:创建任务分支 `task/add_casino_games_2025-01-14_1` 和任务文件
- 原因按照RIPER-5协议建立工作基础
- 阻碍因素:无
- 状态:成功
[2025-10-30_16:30:00](预估时间)
- 已修改:完成详细技术规划
- 更改:设计数据库表结构、游戏流程、抽水机制等细节
- 原因:为实施阶段提供详细技术规范
- 阻碍因素:无
- 状态:成功
[2025-10-30_16:07:57]
- 已修改core/database.py, games/casino.py, utils/parser.py, routers/callback.py, games/base.py
- 更改完成所有实施步骤1-19
- 添加赌场表创建和索引
- 实现6个数据库方法create_casino_session, get_active_casino_session, create_casino_bet, get_pending_bets, settle_casino_bets, close_casino_session
- 创建完整的CasinoGame类实现大小游戏所有功能
- 注册指令映射和路由
- 添加帮助信息
- 原因:按照详细实施计划完成全部功能开发
- 阻碍因素:无
- 状态:成功
[2025-10-30_17:20:00](预估时间)
- 已修改games/casino.py
- 更改:修改结算逻辑,从庄家指定结果改为系统随机生成
- 移除庄家输入种子/结果的参数
- 使用random.random()生成随机结果50%大/50%小)
- 更新帮助信息settle命令不再需要参数
- 原因:用户反馈庄家不应该能够操控游戏结果,庄家也是玩家
- 阻碍因素:无
- 状态:成功
[2025-10-30_17:26:19]
- 已修改games/casino.py, games/base.py
- 更改:添加庄家放弃游戏功能
- 新增_cancel_bigsmall()方法处理放弃逻辑
- 放弃时返还所有玩家下注
- 关闭会话并标记下注为cancelled
- 添加cancel命令支持cancel/放弃/关闭)
- 更新帮助信息和base.py中的帮助
- 原因:用户要求庄家可以放弃本轮游戏
- 阻碍因素:无
- 状态:成功
[2025-10-31_11:35:18]
- 已修改core/database.py
- 更改扩展数据库支持轮盘和21点游戏
- 添加列存在性检查辅助方法_column_exists, _add_column_if_not_exists
- 扩展casino_sessions表添加current_phase和blackjack_multiplier字段兼容性检查
- 扩展casino_bets表添加bet_category、bet_number、bet_value、hand_status字段兼容性检查
- 创建casino_blackjack_hands表存储21点游戏手牌数据
- 修改create_casino_session()支持单场限制检查get_any_active_casino_session和新字段
- 扩展create_casino_bet()支持轮盘和21点专用字段参数
- 添加21点手牌管理方法create_blackjack_hand、get_blackjack_hand、update_blackjack_hand、get_all_blackjack_hands
- 原因为轮盘和21点游戏提供数据库支持确保字段分离和向后兼容
- 阻碍因素:无
- 状态:成功
[2025-10-31_15:15:08]
- 已修改core/database.py
- 更改修复大小游戏结算时的UNIQUE约束冲突问题
- 移除casino_sessions表的UNIQUE(chat_id, game_type, status)约束
- 原因status='closed'时需要允许多条历史记录UNIQUE约束阻止了结算时更新status
- 添加兼容性迁移逻辑检测旧版本表结构自动重建表以移除UNIQUE约束
- 迁移时复制所有历史数据,处理外键关系(临时禁用/启用外键检查)
- 单场限制通过应用层逻辑get_any_active_casino_session保证
- 原因:用户测试大小游戏结算时遇到"UNIQUE constraint failed"错误
- 阻碍因素:无
- 状态:成功
[2025-10-31_15:15:08]
- 已修改core/database.py
- 更改修复21点游戏结算逻辑问题
- 修正losers统计逻辑将条件从`not player_is_busted and player_points != banker_points`改为`player_points != banker_points`
- 原因原条件排除了爆牌玩家导致爆牌玩家未被统计到losers列表
- 修正数据库更新逻辑:明确区分三种情况
- 赢家:发放奖励并更新数据库
- 平局player_points == banker_points已返还下注更新数据库
- 输家else分支包括爆牌和点数小于庄家更新数据库
- 改进结果字符串显示:包含玩家和庄家的状态信息(爆牌、黑杰克等)
- 例如:"庄家19点 vs 玩家爆牌" 或 "庄家19点 vs 玩家20点黑杰克"
- 原因用户测试21点游戏时发现3人游戏中只有1个赢家被结算1个爆牌玩家和1个平局玩家未被结算
- 阻碍因素:无
- 状态:成功
[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'错误
- 阻碍因素:无
- 状态:成功
# 最终审查
待完成

View File

@@ -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`(包含时分秒)
- 原因:修复跨天冒险无法正确显示完成时间的问题,只要跨天就显示完整日期,确保秒数清晰显示
- 阻碍因素:无
- 状态:成功
# 最终审查

View File

@@ -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发送提示消息保持私密性
- 原因:
实现用户可用的私聊功能,作为私聊功能的开始
- 阻碍因素:
- 状态:成功(测试通过)
# 最终审查
待审查阶段完成...

View File

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

View 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

Submodule PWF updated: ca3cf114e3...9cb259f2c7

View File

@@ -5,6 +5,9 @@ from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ProjectConfig from PWF.Convention.Runtime.GlobalConfig import ProjectConfig
from PWF.Convention.Runtime.Web import ToolURL from PWF.Convention.Runtime.Web import ToolURL
from PWF.Convention.Runtime.String import LimitStringLength 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 httpx
import re import re
@@ -13,6 +16,613 @@ MAIN_WEBHOOK_URL = logger.FindItem("main_webhook_url", "")
logger.SaveProperties() 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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
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: class MessageSender:
"""消息发送器""" """消息发送器"""
@@ -190,6 +800,198 @@ class BasicWPSInterface(PluginInterface):
class WPSAPI(BasicWPSInterface): 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 @override
def is_enable_plugin(self) -> bool: def is_enable_plugin(self) -> bool:
if MAIN_WEBHOOK_URL == "": if MAIN_WEBHOOK_URL == "":

View File

@@ -13,7 +13,7 @@ from PWF.CoreModules.database import get_db, STATUS_COMPLETED
from PWF.CoreModules.plugin_interface import DatabaseModel from PWF.CoreModules.plugin_interface import DatabaseModel
from PWF.CoreModules.flags import get_internal_debug from PWF.CoreModules.flags import get_internal_debug
from .WPSAPI import WPSAPI from .WPSAPI import GuideEntry, GuideSection, WPSAPI
from .WPSBackpackSystem import ( from .WPSBackpackSystem import (
BackpackItemDefinition, BackpackItemDefinition,
BackpackItemTier, BackpackItemTier,
@@ -73,6 +73,210 @@ class WPSAlchemyGame(WPSAPI):
self._max_points_per_batch = MAX_POINTS_PER_BATCH self._max_points_per_batch = MAX_POINTS_PER_BATCH
logger.SaveProperties() 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 @override
def dependencies(self) -> List[type]: def dependencies(self) -> List[type]:
return [WPSAPI, WPSBackpackSystem, WPSConfigAPI, WPSFortuneSystem, WPSStoreSystem] return [WPSAPI, WPSBackpackSystem, WPSConfigAPI, WPSFortuneSystem, WPSStoreSystem]
@@ -858,6 +1062,31 @@ class WPSAlchemyRecipeLookup(WPSAPI):
self._alchemy: Optional[WPSAlchemyGame] = None self._alchemy: Optional[WPSAlchemyGame] = None
self._backpack: Optional[WPSBackpackSystem] = 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]: def dependencies(self) -> List[type]:
return [WPSAlchemyGame, WPSBackpackSystem] return [WPSAlchemyGame, WPSBackpackSystem]

View File

@@ -2,14 +2,14 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum 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.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from PWF.CoreModules.database import get_db from PWF.CoreModules.database import get_db
from PWF.CoreModules.flags import get_internal_debug from PWF.CoreModules.flags import get_internal_debug
from .WPSAPI import WPSAPI from .WPSAPI import GuideEntry, GuideSection, WPSAPI
logger: ProjectConfig = Architecture.Get(ProjectConfig) logger: ProjectConfig = Architecture.Get(ProjectConfig)
@@ -58,6 +58,48 @@ class WPSBackpackSystem(WPSAPI):
ITEMS_TABLE = "backpack_items" ITEMS_TABLE = "backpack_items"
USER_ITEMS_TABLE = "backpack_user_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: def __init__(self) -> None:
super().__init__() super().__init__()
self._item_cache: Dict[str, BackpackItemDefinition] = {} self._item_cache: Dict[str, BackpackItemDefinition] = {}
@@ -124,6 +166,50 @@ class WPSBackpackSystem(WPSAPI):
description=description, 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 对外接口 # region 对外接口
def register_item( def register_item(

View File

@@ -3,11 +3,13 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSAPI import GuideEntry, GuideSection
from .combat_plugin_base import WPSCombatBase from .combat_plugin_base import WPSCombatBase
from .combat_models import CombatConfig
logger: ProjectConfig = ProjectConfig() logger: ProjectConfig = ProjectConfig()
@@ -16,6 +18,133 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatAdventure(WPSCombatBase): 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: def is_enable_plugin(self) -> bool:
return True return True

View File

@@ -2,13 +2,13 @@
from __future__ import annotations 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.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from PWF.CoreModules.plugin_interface import DatabaseModel 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.WPSBackpackSystem import BackpackItemTier, WPSBackpackSystem
from Plugins.WPSStoreSystem import WPSStoreSystem from Plugins.WPSStoreSystem import WPSStoreSystem
from Plugins.WPSConfigSystem import WPSConfigAPI from Plugins.WPSConfigSystem import WPSConfigAPI
@@ -46,6 +46,270 @@ class WPSCombatBase(WPSAPI):
_service: CombatService | None = None _service: CombatService | None = None
_initialized: bool = False _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 @classmethod
def service(cls) -> CombatService: def service(cls) -> CombatService:

View File

@@ -2,10 +2,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSAPI import GuideEntry
from .combat_plugin_base import WPSCombatBase from .combat_plugin_base import WPSCombatBase
@@ -15,6 +16,69 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatBattle(WPSCombatBase): class WPSCombatBattle(WPSCombatBase):
"""PVP对战插件""" """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: def is_enable_plugin(self) -> bool:
return True return True

View File

@@ -2,11 +2,12 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem from Plugins.WPSBackpackSystem import WPSBackpackSystem
from Plugins.WPSConfigSystem import WPSConfigAPI 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() 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: def is_enable_plugin(self) -> bool:
return True return True

View File

@@ -2,10 +2,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from Plugins.WPSAPI import GuideEntry
from .combat_plugin_base import WPSCombatBase from .combat_plugin_base import WPSCombatBase
@@ -15,6 +16,42 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatEquipment(WPSCombatBase): 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: def is_enable_plugin(self) -> bool:
return True return True

View File

@@ -2,9 +2,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig 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 from .combat_plugin_base import WPSCombatBase
@@ -15,6 +17,32 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatHeal(WPSCombatBase): 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: def is_enable_plugin(self) -> bool:
return True return True

View File

@@ -2,11 +2,12 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.Config import * from PWF.Convention.Runtime.Config import *
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig 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 from .combat_plugin_base import WPSCombatBase
@@ -16,6 +17,44 @@ logger: ProjectConfig = ProjectConfig()
class WPSCombatStatus(WPSCombatBase): 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: def is_enable_plugin(self) -> bool:
return True return True

View File

@@ -14,6 +14,41 @@ CHECKIN_POINTS = logger.FindItem("checkin_points", 100)
logger.SaveProperties() logger.SaveProperties()
class WPSConfigAPI(WPSAPI): 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 @override
def dependencies(self) -> List[Type]: def dependencies(self) -> List[Type]:
return [WPSAPI] return [WPSAPI]
@@ -196,6 +231,37 @@ class WPSConfigAPI(WPSAPI):
class WPSCheckinAPI(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 @override
def dependencies(self) -> List[Type]: def dependencies(self) -> List[Type]:
return [WPSAPI] return [WPSAPI]

View File

@@ -53,6 +53,54 @@ class WPSCrystalSystem(WPSAPI):
key.lower(): value for key, value in DEFAULT_CRYSTAL_EXCHANGE_ENTRIES.items() 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 # Plugin lifecycle
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #

View File

@@ -32,6 +32,35 @@ _FORTUNE_STAGE_TABLE: List[Tuple[float, str]] = [
class WPSFortuneSystem(WPSAPI): 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 @override
def dependencies(self) -> List[Type]: def dependencies(self) -> List[Type]:
return [WPSAPI] return [WPSAPI]

View File

@@ -3,13 +3,13 @@
from __future__ import annotations from __future__ import annotations
import json 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.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from PWF.CoreModules.plugin_interface import DatabaseModel from PWF.CoreModules.plugin_interface import DatabaseModel
from Plugins.WPSAPI import WPSAPI from Plugins.WPSAPI import GuideEntry, GuideSection, WPSAPI
from Plugins.WPSBackpackSystem import ( from Plugins.WPSBackpackSystem import (
BackpackItemTier, BackpackItemTier,
WPSBackpackSystem, WPSBackpackSystem,
@@ -33,6 +33,166 @@ class WPSGardenBase(WPSAPI):
_service: GardenService | None = None _service: GardenService | None = None
_initialized: bool = False _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 @classmethod
def service(cls) -> GardenService: def service(cls) -> GardenService:
if cls._service is None: if cls._service is None:

View File

@@ -2,10 +2,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture from PWF.Convention.Runtime.Architecture import Architecture
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem from Plugins.WPSBackpackSystem import WPSBackpackSystem
from Plugins.WPSConfigSystem import WPSConfigAPI from Plugins.WPSConfigSystem import WPSConfigAPI
from Plugins.WPSFortuneSystem import WPSFortuneSystem from Plugins.WPSFortuneSystem import WPSFortuneSystem
@@ -14,6 +15,42 @@ from .garden_plugin_base import WPSGardenBase
class WPSGardenHarvest(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: def wake_up(self) -> None:
super().wake_up() super().wake_up()
self.register_plugin("harvest") self.register_plugin("harvest")

View File

@@ -2,16 +2,54 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture from PWF.Convention.Runtime.Architecture import Architecture
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem from Plugins.WPSBackpackSystem import WPSBackpackSystem
from .garden_plugin_base import WPSGardenBase from .garden_plugin_base import WPSGardenBase
class WPSGardenPlant(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: def wake_up(self) -> None:
super().wake_up() super().wake_up()
self.register_plugin("plant") self.register_plugin("plant")

View File

@@ -2,12 +2,44 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from Plugins.WPSAPI import GuideEntry
from .garden_plugin_base import WPSGardenBase from .garden_plugin_base import WPSGardenBase
class WPSGardenRemove(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: def wake_up(self) -> None:
super().wake_up() super().wake_up()
self.register_plugin("remove") self.register_plugin("remove")

View File

@@ -2,17 +2,55 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture from PWF.Convention.Runtime.Architecture import Architecture
from PWF.CoreModules.database import get_db from PWF.CoreModules.database import get_db
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem from Plugins.WPSBackpackSystem import WPSBackpackSystem
from .garden_plugin_base import WPSGardenBase from .garden_plugin_base import WPSGardenBase
class WPSGardenSteal(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: def wake_up(self) -> None:
super().wake_up() super().wake_up()
self.register_plugin("steal") self.register_plugin("steal")

View File

@@ -2,10 +2,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Sequence
from PWF.Convention.Runtime.Architecture import Architecture from PWF.Convention.Runtime.Architecture import Architecture
from Plugins.WPSAPI import GuideEntry
from Plugins.WPSBackpackSystem import WPSBackpackSystem from Plugins.WPSBackpackSystem import WPSBackpackSystem
from Plugins.WPSConfigSystem import WPSConfigAPI from Plugins.WPSConfigSystem import WPSConfigAPI
@@ -13,6 +14,58 @@ from .garden_plugin_base import WPSGardenBase
class WPSGardenView(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: def wake_up(self) -> None:
super().wake_up() super().wake_up()
self.register_plugin("garden") self.register_plugin("garden")

View File

@@ -83,6 +83,42 @@ class WPSStoreSystem(WPSAPI):
logger.SaveProperties() logger.SaveProperties()
self._permanent_mode_ids: set[str] = set() 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 @override
def dependencies(self) -> List[type]: def dependencies(self) -> List[type]:
return [WPSAPI, WPSConfigAPI, WPSBackpackSystem] return [WPSAPI, WPSConfigAPI, WPSBackpackSystem]
@@ -960,6 +996,31 @@ class WPSStoreSystem(WPSAPI):
class WPSStoreBuyCommand(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 @override
def dependencies(self) -> list[type]: def dependencies(self) -> list[type]:
return [WPSStoreSystem] return [WPSStoreSystem]
@@ -1016,6 +1077,31 @@ class WPSStoreBuyCommand(WPSAPI):
class WPSStoreSellCommand(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 @override
def dependencies(self) -> list[type]: def dependencies(self) -> list[type]:
return [WPSStoreSystem] return [WPSStoreSystem]