Release beta 1.0.3 Mora wallet updates
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## beta 1.0.3.1016 - 2026-10-16
|
||||||
|
|
||||||
|
- Rename the virtual currency user-facing copy to Mora (`摩拉`) while keeping
|
||||||
|
the existing `/金币`, `/金币排行`, and `/Taffy金币` commands as aliases.
|
||||||
|
- Add recent account movement details to `/摩拉` and `/金币`, covering the
|
||||||
|
latest transfer, loan, repayment, lottery, draw, and blackjack net changes.
|
||||||
|
- Suppress short-window semantic duplicate OCR events so one visible command
|
||||||
|
bubble cannot trigger the same local reply multiple times under new event IDs.
|
||||||
|
|
||||||
## beta 1.0.2.1016 - 2026-10-16
|
## beta 1.0.2.1016 - 2026-10-16
|
||||||
|
|
||||||
- Cache OCR-negative visible bubbles and reuse stable visual signatures as the
|
- Cache OCR-negative visible bubbles and reuse stable visual signatures as the
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
|||||||
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
||||||
并通过带视觉确认的 GUI 发送链路回复。
|
并通过带视觉确认的 GUI 发送链路回复。
|
||||||
|
|
||||||
当前版本:`beta 1.0.2.1016`,更新日期:2026-10-16。
|
当前版本:`beta 1.0.3.1016`,更新日期:2026-10-16。
|
||||||
|
|
||||||
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
|
1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
|
||||||
Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major
|
Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major
|
||||||
自走棋、生产部署与回滚门禁。CS Major 自走棋的选手、教练、叙事和规则
|
自走棋、生产部署与回滚门禁。CS Major 自走棋的选手、教练、叙事和规则
|
||||||
已经外置为包内 JSON,虚拟金币支持转账、贷款、还款和群内排行榜。
|
已经外置为包内 JSON,虚拟摩拉支持转账、贷款、还款和群内排行榜。
|
||||||
|
|
||||||
完整命令见[命令目录](docs/command-catalog.md)。历史版本记录见
|
完整命令见[命令目录](docs/command-catalog.md)。历史版本记录见
|
||||||
[CHANGELOG](CHANGELOG.md)。
|
[CHANGELOG](CHANGELOG.md)。
|
||||||
@@ -82,8 +82,8 @@ PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
|
|||||||
/功能列表
|
/功能列表
|
||||||
/上下文 [100|200|300](/context)
|
/上下文 [100|200|300](/context)
|
||||||
/人物列表 [页码](/people);/人物 [名称或别名](/person)
|
/人物列表 [页码](/people);/人物 [名称或别名](/person)
|
||||||
/金币(/wallet);/抽奖 [金额](/draw)
|
/摩拉(兼容 /金币、/wallet);/抽奖 [金额](/draw)
|
||||||
/大富翁榜(/金币排行)
|
/大富翁榜(/摩拉排行,兼容 /金币排行)
|
||||||
/转账 对方 金额(/transfer name amount)
|
/转账 对方 金额(/transfer name amount)
|
||||||
/贷款 [金额](/loan amount);/还款 [金额|全部](/repay)
|
/贷款 [金额](/loan amount);/还款 [金额|全部](/repay)
|
||||||
/彩票 开始|报名|当前状态|开奖|取消(/lottery)
|
/彩票 开始|报名|当前状态|开奖|取消(/lottery)
|
||||||
@@ -263,7 +263,7 @@ visually confirmed GUI bridge.
|
|||||||
Current version:
|
Current version:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
beta 1.0.2.1016
|
beta 1.0.3.1016
|
||||||
```
|
```
|
||||||
|
|
||||||
Updated: 2026-10-16.
|
Updated: 2026-10-16.
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ descriptor inventory; plugin code delegates to reviewed service profiles.
|
|||||||
|
|
||||||
| Command | Alias | Purpose |
|
| Command | Alias | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/金币` | `/wallet` | Initialize or show the current person's virtual coin account. |
|
| `/摩拉` | `/金币`, `/wallet` | Initialize or show the current person's Mora account. |
|
||||||
| `/大富翁榜 [人数]` | `/金币排行`, `/金币排行榜`, `/richlist` | Show the group-scoped coin leaderboard. |
|
| `/大富翁榜 [人数]` | `/摩拉排行`, `/摩拉排行榜`, `/金币排行`, `/金币排行榜`, `/richlist` | Show the group-scoped Mora leaderboard. |
|
||||||
| `/转账 对方 金额` | `/transfer name amount` | Transfer virtual coins to an initialized group account. |
|
| `/转账 对方 金额` | `/transfer name amount` | Transfer virtual coins to an initialized group account. |
|
||||||
| `/贷款 [金额]` | `/loan amount`, `/借款` | Borrow up to 500 virtual coins. |
|
| `/贷款 [金额]` | `/loan amount`, `/借款` | Borrow up to 500 virtual coins. |
|
||||||
| `/还款 [金额|全部]` | `/repay [amount|all]` | Repay an active virtual coin loan. |
|
| `/还款 [金额|全部]` | `/repay [amount|all]` | Repay an active virtual coin loan. |
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
每位玩家开局拥有:
|
每位玩家开局拥有:
|
||||||
|
|
||||||
- 生命值:`100`
|
- 生命值:`100`
|
||||||
- 金币:`10`
|
- 摩拉:`10`
|
||||||
- 教练:赛前第一轮选秀决定
|
- 教练:赛前第一轮选秀决定
|
||||||
- 初始选手:赛前第二、三轮选秀决定,系统随后补齐阵容
|
- 初始选手:赛前第二、三轮选秀决定,系统随后补齐阵容
|
||||||
- 阵容位:固定 `5 个选手位 + 1 个教练位`,比赛时必须全部上场
|
- 阵容位:固定 `5 个选手位 + 1 个教练位`,比赛时必须全部上场
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
|
|
||||||
### 7.2 教练获取
|
### 7.2 教练获取
|
||||||
|
|
||||||
教练由第一轮公共选秀决定,本届赛事内不能用金币升级教练。不同教练
|
教练由第一轮公共选秀决定,本届赛事内不能用摩拉升级教练。不同教练
|
||||||
自带等级、属性倾向和全队加成。
|
自带等级、属性倾向和全队加成。
|
||||||
高费选手的商店概率随赛事进程推进,不再绑定教练等级。
|
高费选手的商店概率随赛事进程推进,不再绑定教练等级。
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
|
|
||||||
- 第一轮教练选秀展示固定 10 名教练;第二、三轮选手选秀展示 6 名选手。
|
- 第一轮教练选秀展示固定 10 名教练;第二、三轮选手选秀展示 6 名选手。
|
||||||
- 训练赛和正赛运营阶段默认展示一组 6 人商店。
|
- 训练赛和正赛运营阶段默认展示一组 6 人商店。
|
||||||
- 玩家可支付金币执行刷新。
|
- 玩家可支付摩拉执行刷新。
|
||||||
- 玩家可锁定本轮 6 人,使下一轮仍保留同一组候选。
|
- 玩家可锁定本轮 6 人,使下一轮仍保留同一组候选。
|
||||||
|
|
||||||
### 8.4 顺位
|
### 8.4 顺位
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
## 10. 训练赛与赛事挑战
|
## 10. 训练赛与赛事挑战
|
||||||
|
|
||||||
训练赛不再是阈值型 PvE。它是正式小组赛前的多轮真实对阵,用来给玩家
|
训练赛不再是阈值型 PvE。它是正式小组赛前的多轮真实对阵,用来给玩家
|
||||||
检验阵容、获取金币并自然插入 D 牌窗口。媒体日和赞助挑战仍作为赛事内
|
检验阵容、获取摩拉并自然插入 D 牌窗口。媒体日和赞助挑战仍作为赛事内
|
||||||
插曲出现,但只提供资源变动,不淘汰玩家。
|
插曲出现,但只提供资源变动,不淘汰玩家。
|
||||||
|
|
||||||
### 10.1 活动类型
|
### 10.1 活动类型
|
||||||
@@ -308,11 +308,11 @@
|
|||||||
|
|
||||||
训练赛按参赛队伍生成 round-robin 轮次。四队测试局会有三轮训练赛,
|
训练赛按参赛队伍生成 round-robin 轮次。四队测试局会有三轮训练赛,
|
||||||
每队在训练阶段遇到另外三支队伍;奇数队伍时轮空队会与本地陪练队热身。
|
每队在训练阶段遇到另外三支队伍;奇数队伍时轮空队会与本地陪练队热身。
|
||||||
训练赛只发放金币,不记录小组胜负,也不扣生命。
|
训练赛只发放摩拉,不记录小组胜负,也不扣生命。
|
||||||
|
|
||||||
每轮训练赛结束后进入运营窗口:
|
每轮训练赛结束后进入运营窗口:
|
||||||
|
|
||||||
- `/刷新` 即 D 牌,消耗金币换一组 6 人商店;
|
- `/刷新` 即 D 牌,消耗摩拉换一组 6 人商店;
|
||||||
- `/选1~/选6` 买入商店选手,重复卡自动累积星级;
|
- `/选1~/选6` 买入商店选手,重复卡自动累积星级;
|
||||||
- `/锁定` 保留当前商店;
|
- `/锁定` 保留当前商店;
|
||||||
- `/上阵` 调整 5 人首发;
|
- `/上阵` 调整 5 人首发;
|
||||||
@@ -321,10 +321,10 @@
|
|||||||
### 10.3 训练赛结算逻辑
|
### 10.3 训练赛结算逻辑
|
||||||
|
|
||||||
训练赛使用与正式比赛相同的单图 MR15 逐回合模拟,只是赛制为 BO1。
|
训练赛使用与正式比赛相同的单图 MR15 逐回合模拟,只是赛制为 BO1。
|
||||||
胜方获得更多金币,负方也有保底金币,保证测试局能继续运营。
|
胜方获得更多摩拉,负方也有保底摩拉,保证测试局能继续运营。
|
||||||
|
|
||||||
媒体日和赞助挑战根据阵容完整度、教练、战术和本地确定性随机因子发放
|
媒体日和赞助挑战根据阵容完整度、教练、战术和本地确定性随机因子发放
|
||||||
金币,但播报不展示“目标/发挥”这类计算日志,只写剧情和奖励。
|
摩拉,但播报不展示“目标/发挥”这类计算日志,只写剧情和奖励。
|
||||||
|
|
||||||
## 11. PvP 赛制
|
## 11. PvP 赛制
|
||||||
|
|
||||||
@@ -618,7 +618,7 @@ Jacky 选择了 zonic!
|
|||||||
- AI 测试战队加入
|
- AI 测试战队加入
|
||||||
- 赛制与种子公布
|
- 赛制与种子公布
|
||||||
- 三轮选秀候选池、选择结果和选秀完成
|
- 三轮选秀候选池、选择结果和选秀完成
|
||||||
- 每轮训练赛对阵、比分、金币和运营提示
|
- 每轮训练赛对阵、比分、摩拉和运营提示
|
||||||
- 媒体日或赞助挑战
|
- 媒体日或赞助挑战
|
||||||
- 每场 BO3/BO5 对阵与结果
|
- 每场 BO3/BO5 对阵与结果
|
||||||
- 每张加时地图
|
- 每张加时地图
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
1. 任意群友发送 `/彩票 开始`。发起人是本轮主持人。
|
1. 任意群友发送 `/彩票 开始`。发起人是本轮主持人。
|
||||||
2. 每个人发送 `/彩票 报名 3 4 7 100`,表示选择 3、4、7,下注 100
|
2. 每个人发送 `/彩票 报名 3 4 7 100`,表示选择 3、4、7,下注 100
|
||||||
金币。也可简写为 `/彩票347100`;省略金额时默认下注 10。
|
摩拉。也可简写为 `/彩票347100`;省略金额时默认下注 10。
|
||||||
3. 发送 `/彩票 当前状态` 查看本轮主持人、报名人数、每个人的号码和下注。
|
3. 发送 `/彩票 当前状态` 查看本轮主持人、报名人数、每个人的号码和下注。
|
||||||
4. 主持人发送 `/彩票 开奖`。机器人从 0~9 中抽取三个互不重复的数字,
|
4. 主持人发送 `/彩票 开奖`。机器人从 0~9 中抽取三个互不重复的数字,
|
||||||
统一排序后结算。
|
统一排序后结算。
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
| 2 个 | 120% |
|
| 2 个 | 120% |
|
||||||
| 3 个 | 300% |
|
| 3 个 | 300% |
|
||||||
|
|
||||||
金币只使用整数;百分比计算的小数部分向下取整。例如下注 100,命中两个
|
摩拉只使用整数;百分比计算的小数部分向下取整。例如下注 100,命中两个
|
||||||
返还 120,净变化为 `+20`。
|
返还 120,净变化为 `+20`。
|
||||||
|
|
||||||
## 个人局号与记录
|
## 个人局号与记录
|
||||||
|
|||||||
+16
-16
@@ -7,8 +7,8 @@ OpenClaw;中文、英文命令对应同一实现,英文不区分大小写,
|
|||||||
| 插件 | 中文指令 | 英文指令 |
|
| 插件 | 中文指令 | 英文指令 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| context-summary | `/上下文 [100|200|300]` | `/context [100|200|300]` |
|
| context-summary | `/上下文 [100|200|300]` | `/context [100|200|300]` |
|
||||||
| single-player-games | `/金币` | `/wallet` |
|
| single-player-games | `/摩拉`(兼容 `/金币`) | `/wallet` |
|
||||||
| coin-ranking | `/大富翁榜`、`/金币排行` | `/richlist` |
|
| coin-ranking | `/大富翁榜`、`/摩拉排行`(兼容 `/金币排行`) | `/richlist` |
|
||||||
| coin-transfer | `/转账 对方 金额` | `/transfer name amount` |
|
| coin-transfer | `/转账 对方 金额` | `/transfer name amount` |
|
||||||
| coin-loan | `/贷款 [金额]`、`/还款 [金额\|全部]` | `/loan amount`、`/repay [amount\|all]` |
|
| coin-loan | `/贷款 [金额]`、`/还款 [金额\|全部]` | `/loan amount`、`/repay [amount\|all]` |
|
||||||
| single-player-games | `/抽奖 [金额]` | `/draw [金额]` |
|
| single-player-games | `/抽奖 [金额]` | `/draw [金额]` |
|
||||||
@@ -23,7 +23,7 @@ OpenClaw;中文、英文命令对应同一实现,英文不区分大小写,
|
|||||||
命令所有权来自 `descriptors.py`,由同步工具生成三套受审核 SDK adapter。
|
命令所有权来自 `descriptors.py`,由同步工具生成三套受审核 SDK adapter。
|
||||||
每个 adapter 只提交命名 service-profile 请求,由 Kernel 执行数据库操作。
|
每个 adapter 只提交命名 service-profile 请求,由 Kernel 执行数据库操作。
|
||||||
注册在启动期完成,运行期冻结。`/余额` 保持查询提供商余额;
|
注册在启动期完成,运行期冻结。`/余额` 保持查询提供商余额;
|
||||||
游戏虚拟余额使用 `/金币`。
|
游戏虚拟余额使用 `/摩拉`,`/金币` 保留为兼容别名。
|
||||||
|
|
||||||
## 上下文总结
|
## 上下文总结
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ OpenClaw;中文、英文命令对应同一实现,英文不区分大小写,
|
|||||||
|
|
||||||
当前 OCR 输入只有显示名,没有微信原生账号 ID。Kernel 将昵称或别名
|
当前 OCR 输入只有显示名,没有微信原生账号 ID。Kernel 将昵称或别名
|
||||||
解析到私有配置中的 canonical_name,再以“当前群+规范人物名”计算
|
解析到私有配置中的 canonical_name,再以“当前群+规范人物名”计算
|
||||||
person_key。相同人物的已配置别名共用金币和牌局,其他群使用独立账户。
|
person_key。相同人物的已配置别名共用摩拉和牌局,其他群使用独立账户。
|
||||||
没有配置的显示名使用独立临时身份;未识别到发送人的消息不能操作账户。
|
没有配置的显示名使用独立临时身份;未识别到发送人的消息不能操作账户。
|
||||||
|
|
||||||
`/人物` 默认查看本人;指定参数时只查询已配置人物。输出包括展示名、
|
`/人物` 默认查看本人;指定参数时只查询已配置人物。输出包括展示名、
|
||||||
@@ -53,27 +53,27 @@ person_key。相同人物的已配置别名共用金币和牌局,其他群使
|
|||||||
|
|
||||||
昵称识别不是账号身份认证。修改 canonical_name 会改变当前人物作用域;
|
昵称识别不是账号身份认证。修改 canonical_name 会改变当前人物作用域;
|
||||||
应优先追加别名而不是重命名。除 `/转账` 外,用户不能通过游戏命令
|
应优先追加别名而不是重命名。除 `/转账` 外,用户不能通过游戏命令
|
||||||
指定他人的账户;转账只支持同群虚拟金币账户,收款人需先发送 `/金币`
|
指定他人的账户;转账只支持同群虚拟摩拉账户,收款人需先发送 `/摩拉`
|
||||||
初始化。`/贷款` 和 `/还款` 只作用于本人账户。虚拟金币不能充值、
|
或 `/金币` 初始化。`/贷款` 和 `/还款` 只作用于本人账户。虚拟摩拉不能充值、
|
||||||
赠币或兑换真实资产。
|
赠币或兑换真实资产。
|
||||||
|
|
||||||
## 虚拟金币与游戏管理
|
## 虚拟摩拉与游戏管理
|
||||||
|
|
||||||
每个群内人物首次使用账户时一次性获得 1000 虚拟金币。再次查询、
|
每个群内人物首次使用账户时一次性获得 1000 虚拟摩拉。再次查询、
|
||||||
重启服务或切换已配置别名都不会重复领取。抽奖下注为 2~100,彩票和
|
重启服务或切换已配置别名都不会重复领取。抽奖下注为 2~100,彩票和
|
||||||
21 点下注为 1~500;余额不足或参数非法不扣款。
|
21 点下注为 1~500;余额不足或参数非法不扣款。
|
||||||
|
|
||||||
`/转账 对方 金额` 会把本人的虚拟金币转给同群已初始化账户,英文为
|
`/转账 对方 金额` 会把本人的虚拟摩拉转给同群已初始化账户,英文为
|
||||||
`/transfer name amount`。金额范围 1~100000;不允许给自己转账。
|
`/transfer name amount`。金额范围 1~100000;不允许给自己转账。
|
||||||
转账、响应记录和重试去重写在同一个 ledger 事务内,同一 event_id
|
转账、响应记录和重试去重写在同一个 ledger 事务内,同一 event_id
|
||||||
不会重复扣款。
|
不会重复扣款。
|
||||||
|
|
||||||
`/贷款 [金额]` 会给本人账户借入 1~500 金币;同一账户同一时间最多
|
`/贷款 [金额]` 会给本人账户借入 1~500 摩拉;同一账户同一时间最多
|
||||||
一笔未还贷款。`/还款 [金额|全部]` 从当前金币余额中扣除并减少未还
|
一笔未还贷款。`/还款 [金额|全部]` 从当前摩拉余额中扣除并减少未还
|
||||||
贷款,借款、还款、响应记录和重试去重同样写在一个 ledger 事务内。
|
贷款,借款、还款、响应记录和重试去重同样写在一个 ledger 事务内。
|
||||||
|
|
||||||
`/大富翁榜` 或 `/金币排行 [人数]` 只读取当前群已有金币账户,不会创建
|
`/大富翁榜` 或 `/摩拉排行 [人数]`(兼容 `/金币排行`)只读取当前群已有摩拉账户,不会创建
|
||||||
调用者账户。榜单按当前金币余额降序排列,人数范围 1~20;账户存在
|
调用者账户。榜单按当前摩拉余额降序排列,人数范围 1~20;账户存在
|
||||||
未还贷款时会在名字后标注贷款本金。
|
未还贷款时会在名字后标注贷款本金。
|
||||||
|
|
||||||
抽奖默认下注 10:5% 概率返还五倍下注,20% 返还两倍,其余无返还。
|
抽奖默认下注 10:5% 概率返还五倍下注,20% 返还两倍,其余无返还。
|
||||||
@@ -81,7 +81,7 @@ alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数
|
|||||||
下注范围 1~500,命中 1/2/3 个分别返还 50%/120%/300%。完整流程见
|
下注范围 1~500,命中 1/2/3 个分别返还 50%/120%/300%。完整流程见
|
||||||
[多人彩票与游戏记录](group-lottery-0.6.3.md)。
|
[多人彩票与游戏记录](group-lottery-0.6.3.md)。
|
||||||
|
|
||||||
21 点默认下注 50、范围 1~500。`/21点 开始 100` 表示拿 100 金币作为本局下注进场,
|
21 点默认下注 50、范围 1~500。`/21点 开始 100` 表示拿 100 摩拉作为本局下注进场,
|
||||||
等价的 OCR 紧凑形式是 `/21点开始100`。开局会扣除下注并发两张牌,
|
等价的 OCR 紧凑形式是 `/21点开始100`。开局会扣除下注并发两张牌,
|
||||||
然后根据牌面单独发送 `/21点 要牌` 或 `/21点 停牌`;开局与操作写在
|
然后根据牌面单独发送 `/21点 要牌` 或 `/21点 停牌`;开局与操作写在
|
||||||
同一条消息时不会扣款,而会返回分步提示。牌局使用洗牌后的单副 52 张牌,
|
同一条消息时不会扣款,而会返回分步提示。牌局使用洗牌后的单副 52 张牌,
|
||||||
@@ -95,7 +95,7 @@ alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数
|
|||||||
其他玩家在下注前用 `/21点 加入` 进入当前牌桌;主持人发送 `/21点 多人 开始`
|
其他玩家在下注前用 `/21点 加入` 进入当前牌桌;主持人发送 `/21点 多人 开始`
|
||||||
后,Taffy 会按座位顺序提示每个人 `/21点 放200` 放筹码,范围 1~10000。
|
后,Taffy 会按座位顺序提示每个人 `/21点 放200` 放筹码,范围 1~10000。
|
||||||
所有玩家下注后统一发牌,轮到本人时用 `/21点 要牌` 或 `/21点 停牌` 操作;
|
所有玩家下注后统一发牌,轮到本人时用 `/21点 要牌` 或 `/21点 停牌` 操作;
|
||||||
结算按座位顺序列出每个人的下注、返还、净变化和当前金币。
|
结算按座位顺序列出每个人的下注、返还、净变化和当前摩拉。
|
||||||
自然 21 点立即结算,返还 2.5 倍下注向下取整;普通胜利返还两倍,
|
自然 21 点立即结算,返还 2.5 倍下注向下取整;普通胜利返还两倍,
|
||||||
平局返还下注,失败不返还。每人最多一局;牌局进行中不接受另一笔
|
平局返还下注,失败不返还。每人最多一局;牌局进行中不接受另一笔
|
||||||
抽奖或 21 点开局,多人彩票报名仍可独立进行。
|
抽奖或 21 点开局,多人彩票报名仍可独立进行。
|
||||||
@@ -106,7 +106,7 @@ alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数
|
|||||||
|
|
||||||
Kernel 用一个 `BEGIN IMMEDIATE` 事务同时保存:
|
Kernel 用一个 `BEGIN IMMEDIATE` 事务同时保存:
|
||||||
|
|
||||||
1. `player_accounts` 中的金币、牌局和胜负统计;
|
1. `player_accounts` 中的摩拉、牌局和胜负统计;
|
||||||
2. `player_actions` 中的事件、命令及响应。
|
2. `player_actions` 中的事件、命令及响应。
|
||||||
|
|
||||||
同一群中的同一 event_id 只结算一次,重试返回原响应。相同事件若变更
|
同一群中的同一 event_id 只结算一次,重试返回原响应。相同事件若变更
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ Help:
|
|||||||
|
|
||||||
Registered commands:
|
Registered commands:
|
||||||
|
|
||||||
|
- `/摩拉排行`
|
||||||
|
- `/摩拉排行榜`
|
||||||
- `/金币排行`
|
- `/金币排行`
|
||||||
- `/金币排行榜`
|
- `/金币排行榜`
|
||||||
- `/大富翁榜`
|
- `/大富翁榜`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ entrypoint = "coin_ranking:plugin"
|
|||||||
type = "command"
|
type = "command"
|
||||||
|
|
||||||
[capabilities]
|
[capabilities]
|
||||||
commands = ["/金币排行", "/金币排行榜", "/大富翁榜", "/richlist"]
|
commands = ["/摩拉排行", "/摩拉排行榜", "/金币排行", "/金币排行榜", "/大富翁榜", "/richlist"]
|
||||||
storage = false
|
storage = false
|
||||||
network_profiles = []
|
network_profiles = []
|
||||||
service_profiles = ["coin-ranking"]
|
service_profiles = ["coin-ranking"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ PROFILE = 'coin-ranking'
|
|||||||
AUTHOR = 'jwebot'
|
AUTHOR = 'jwebot'
|
||||||
DESCRIPTION = '富榜'
|
DESCRIPTION = '富榜'
|
||||||
HELP_ENTRIES = ('/大富翁榜',)
|
HELP_ENTRIES = ('/大富翁榜',)
|
||||||
COMMANDS = ('/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
|
COMMANDS = ('/摩拉排行', '/摩拉排行榜', '/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
|
||||||
|
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
def _handler(context: SingleGroupContext, command: str):
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class PluginTests(unittest.TestCase):
|
|||||||
self.assertEqual(AUTHOR, 'jwebot')
|
self.assertEqual(AUTHOR, 'jwebot')
|
||||||
self.assertEqual(DESCRIPTION, '富榜')
|
self.assertEqual(DESCRIPTION, '富榜')
|
||||||
self.assertEqual(HELP_ENTRIES, ('/大富翁榜',))
|
self.assertEqual(HELP_ENTRIES, ('/大富翁榜',))
|
||||||
self.assertIn('/金币排行', registered)
|
self.assertIn('/摩拉排行', registered)
|
||||||
self.assertEqual(len(registered), 4)
|
self.assertEqual(len(registered), 6)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
Author: `jwebot`
|
Author: `jwebot`
|
||||||
|
|
||||||
Taffy金币
|
Taffy摩拉
|
||||||
|
|
||||||
Help:
|
Help:
|
||||||
|
|
||||||
- `/Taffy金币`
|
- `/Taffy摩拉(兼容 /Taffy金币)`
|
||||||
|
|
||||||
Registered commands:
|
Registered commands:
|
||||||
|
|
||||||
|
- `/Taffy摩拉`
|
||||||
- `/Taffy金币`
|
- `/Taffy金币`
|
||||||
- `/taffycoins`
|
- `/taffycoins`
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
id = "coin-taffy"
|
id = "coin-taffy"
|
||||||
author = "jwebot"
|
author = "jwebot"
|
||||||
description = "Taffy金币"
|
description = "Taffy摩拉"
|
||||||
help = ["/Taffy金币"]
|
help = ["/Taffy摩拉(兼容 /Taffy金币)"]
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
api_version = "1"
|
api_version = "1"
|
||||||
entrypoint = "coin_taffy:plugin"
|
entrypoint = "coin_taffy:plugin"
|
||||||
type = "command"
|
type = "command"
|
||||||
|
|
||||||
[capabilities]
|
[capabilities]
|
||||||
commands = ["/Taffy金币", "/taffycoins"]
|
commands = ["/Taffy摩拉", "/Taffy金币", "/taffycoins"]
|
||||||
storage = false
|
storage = false
|
||||||
network_profiles = []
|
network_profiles = []
|
||||||
service_profiles = ["coin-taffy"]
|
service_profiles = ["coin-taffy"]
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
|||||||
|
|
||||||
PROFILE = 'coin-taffy'
|
PROFILE = 'coin-taffy'
|
||||||
AUTHOR = 'jwebot'
|
AUTHOR = 'jwebot'
|
||||||
DESCRIPTION = 'Taffy金币'
|
DESCRIPTION = 'Taffy摩拉'
|
||||||
HELP_ENTRIES = ('/Taffy金币',)
|
HELP_ENTRIES = ('/Taffy摩拉(兼容 /Taffy金币)',)
|
||||||
COMMANDS = ('/Taffy金币', '/taffycoins')
|
COMMANDS = ('/Taffy摩拉', '/Taffy金币', '/taffycoins')
|
||||||
|
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
def _handler(context: SingleGroupContext, command: str):
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ class PluginTests(unittest.TestCase):
|
|||||||
for call in context.commands.register.call_args_list
|
for call in context.commands.register.call_args_list
|
||||||
]
|
]
|
||||||
self.assertEqual(AUTHOR, 'jwebot')
|
self.assertEqual(AUTHOR, 'jwebot')
|
||||||
self.assertEqual(DESCRIPTION, 'Taffy金币')
|
self.assertEqual(DESCRIPTION, 'Taffy摩拉')
|
||||||
self.assertEqual(HELP_ENTRIES, ('/Taffy金币',))
|
self.assertEqual(HELP_ENTRIES, ('/Taffy摩拉(兼容 /Taffy金币)',))
|
||||||
self.assertIn('/Taffy金币', registered)
|
self.assertIn('/Taffy摩拉', registered)
|
||||||
self.assertEqual(len(registered), 2)
|
self.assertEqual(len(registered), 3)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Author: `jwebot`
|
Author: `jwebot`
|
||||||
|
|
||||||
金币转账
|
摩拉转账
|
||||||
|
|
||||||
Help:
|
Help:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
id = "coin-transfer"
|
id = "coin-transfer"
|
||||||
author = "jwebot"
|
author = "jwebot"
|
||||||
description = "金币转账"
|
description = "摩拉转账"
|
||||||
help = ["/转账 对方 金额;/transfer"]
|
help = ["/转账 对方 金额;/transfer"]
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
api_version = "1"
|
api_version = "1"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
|||||||
|
|
||||||
PROFILE = 'coin-transfer'
|
PROFILE = 'coin-transfer'
|
||||||
AUTHOR = 'jwebot'
|
AUTHOR = 'jwebot'
|
||||||
DESCRIPTION = '金币转账'
|
DESCRIPTION = '摩拉转账'
|
||||||
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
|
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
|
||||||
COMMANDS = ('/转账', '/transfer')
|
COMMANDS = ('/转账', '/transfer')
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class PluginTests(unittest.TestCase):
|
|||||||
for call in context.commands.register.call_args_list
|
for call in context.commands.register.call_args_list
|
||||||
]
|
]
|
||||||
self.assertEqual(AUTHOR, 'jwebot')
|
self.assertEqual(AUTHOR, 'jwebot')
|
||||||
self.assertEqual(DESCRIPTION, '金币转账')
|
self.assertEqual(DESCRIPTION, '摩拉转账')
|
||||||
self.assertEqual(HELP_ENTRIES, ('/转账 对方 金额;/transfer',))
|
self.assertEqual(HELP_ENTRIES, ('/转账 对方 金额;/transfer',))
|
||||||
self.assertIn('/转账', registered)
|
self.assertIn('/转账', registered)
|
||||||
self.assertEqual(len(registered), 2)
|
self.assertEqual(len(registered), 2)
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
Author: `jwebot`
|
Author: `jwebot`
|
||||||
|
|
||||||
金币游戏
|
摩拉游戏
|
||||||
|
|
||||||
Help:
|
Help:
|
||||||
|
|
||||||
- `/金币 /wallet;/游戏记录 /gamehistory`
|
- `/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory`
|
||||||
- `/抽奖 [金额];/draw`
|
- `/抽奖 [金额];/draw`
|
||||||
- `/彩票 开始|报名|状态|开奖|取消;/lottery`
|
- `/彩票 开始|报名|状态|开奖|取消;/lottery`
|
||||||
- `/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200`
|
- `/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200`
|
||||||
@@ -14,6 +14,7 @@ Help:
|
|||||||
|
|
||||||
Registered commands:
|
Registered commands:
|
||||||
|
|
||||||
|
- `/摩拉`
|
||||||
- `/金币`
|
- `/金币`
|
||||||
- `/wallet`
|
- `/wallet`
|
||||||
- `/抽奖`
|
- `/抽奖`
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
id = "single-player-games"
|
id = "single-player-games"
|
||||||
author = "jwebot"
|
author = "jwebot"
|
||||||
description = "金币游戏"
|
description = "摩拉游戏"
|
||||||
help = ["/金币 /wallet;/游戏记录 /gamehistory", "/抽奖 [金额];/draw", "/彩票 开始|报名|状态|开奖|取消;/lottery", "/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200", "/游戏帮助;/games"]
|
help = ["/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory", "/抽奖 [金额];/draw", "/彩票 开始|报名|状态|开奖|取消;/lottery", "/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200", "/游戏帮助;/games"]
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
api_version = "1"
|
api_version = "1"
|
||||||
entrypoint = "single_player_games:plugin"
|
entrypoint = "single_player_games:plugin"
|
||||||
type = "command"
|
type = "command"
|
||||||
|
|
||||||
[capabilities]
|
[capabilities]
|
||||||
commands = ["/金币", "/wallet", "/抽奖", "/draw", "/彩票", "/买彩票", "/lottery", "/21点", "/二十一点", "/blackjack", "/游戏记录", "/个人记录", "/gamehistory", "/游戏帮助", "/games"]
|
commands = ["/摩拉", "/金币", "/wallet", "/抽奖", "/draw", "/彩票", "/买彩票", "/lottery", "/21点", "/二十一点", "/blackjack", "/游戏记录", "/个人记录", "/gamehistory", "/游戏帮助", "/games"]
|
||||||
storage = false
|
storage = false
|
||||||
network_profiles = []
|
network_profiles = []
|
||||||
service_profiles = ["single-player-games"]
|
service_profiles = ["single-player-games"]
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
|||||||
|
|
||||||
PROFILE = 'single-player-games'
|
PROFILE = 'single-player-games'
|
||||||
AUTHOR = 'jwebot'
|
AUTHOR = 'jwebot'
|
||||||
DESCRIPTION = '金币游戏'
|
DESCRIPTION = '摩拉游戏'
|
||||||
HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games')
|
HELP_ENTRIES = ('/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games')
|
||||||
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
COMMANDS = ('/摩拉', '/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
||||||
|
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
def _handler(context: SingleGroupContext, command: str):
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ class PluginTests(unittest.TestCase):
|
|||||||
for call in context.commands.register.call_args_list
|
for call in context.commands.register.call_args_list
|
||||||
]
|
]
|
||||||
self.assertEqual(AUTHOR, 'jwebot')
|
self.assertEqual(AUTHOR, 'jwebot')
|
||||||
self.assertEqual(DESCRIPTION, '金币游戏')
|
self.assertEqual(DESCRIPTION, '摩拉游戏')
|
||||||
self.assertEqual(HELP_ENTRIES, ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games'))
|
self.assertEqual(HELP_ENTRIES, ('/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games'))
|
||||||
self.assertIn('/金币', registered)
|
self.assertIn('/摩拉', registered)
|
||||||
self.assertEqual(len(registered), 15)
|
self.assertEqual(len(registered), 16)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-config"
|
name = "jwebot-config"
|
||||||
version = "1.0.2b1+1016"
|
version = "1.0.3b1+1016"
|
||||||
description = "Profile-based configuration compiler for JWeBot"
|
description = "Profile-based configuration compiler for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ __all__ = [
|
|||||||
"write_default_profile",
|
"write_default_profile",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.2b1+1016"
|
__version__ = "1.0.3b1+1016"
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-extension-host"
|
name = "jwebot-extension-host"
|
||||||
version = "1.0.2b1+1016"
|
version = "1.0.3b1+1016"
|
||||||
description = "Single-release, single-invocation process host for JWeBot extensions"
|
description = "Single-release, single-invocation process host for JWeBot extensions"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-sdk==1.0.2b1+1016",
|
"jwebot-sdk==1.0.3b1+1016",
|
||||||
"jwebot-kernel==1.0.2b1+1016",
|
"jwebot-kernel==1.0.3b1+1016",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ __all__ = [
|
|||||||
"process_request",
|
"process_request",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.2b1+1016"
|
__version__ = "1.0.3b1+1016"
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-kernel"
|
name = "jwebot-kernel"
|
||||||
version = "1.0.2b1+1016"
|
version = "1.0.3b1+1016"
|
||||||
description = "Stable routing and extension authority for JWeBot"
|
description = "Stable routing and extension authority for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-config==1.0.2b1+1016",
|
"jwebot-config==1.0.3b1+1016",
|
||||||
"jwebot-sdk==1.0.2b1+1016",
|
"jwebot-sdk==1.0.3b1+1016",
|
||||||
"jwebot-official-extensions==1.0.2b1+1016",
|
"jwebot-official-extensions==1.0.3b1+1016",
|
||||||
"tomli>=2; python_version < '3.11'",
|
"tomli>=2; python_version < '3.11'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -107,4 +107,4 @@ __all__ = [
|
|||||||
"verify_installed_release_signature",
|
"verify_installed_release_signature",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.2b1+1016"
|
__version__ = "1.0.3b1+1016"
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ from jwebot_official_extensions.steam import (
|
|||||||
SteamUnavailable,
|
SteamUnavailable,
|
||||||
)
|
)
|
||||||
from jwebot_official_extensions.player_games import (
|
from jwebot_official_extensions.player_games import (
|
||||||
|
append_coin_event,
|
||||||
|
append_game_coin_event,
|
||||||
append_game_record,
|
append_game_record,
|
||||||
hand_value,
|
hand_value,
|
||||||
next_game_index,
|
next_game_index,
|
||||||
@@ -98,6 +100,7 @@ EXTENSION_SOCKET_ROOT = Path("/run/jwebot-extension-host")
|
|||||||
MAX_EVENT_TEXT = 5_000
|
MAX_EVENT_TEXT = 5_000
|
||||||
MAX_REPLY_CHARS = 900
|
MAX_REPLY_CHARS = 900
|
||||||
MAX_CONTEXT_TURNS = 6
|
MAX_CONTEXT_TURNS = 6
|
||||||
|
OCR_EVENT_SOURCES = frozenset({"ocr", "rapidocr-local"})
|
||||||
MAX_OUTBOX_ATTEMPTS = 3
|
MAX_OUTBOX_ATTEMPTS = 3
|
||||||
DIRECT_CHAT_GENERATION_WORKERS = 2
|
DIRECT_CHAT_GENERATION_WORKERS = 2
|
||||||
DIRECT_API_SOCKET_TIMEOUT_SECONDS = 18.0
|
DIRECT_API_SOCKET_TIMEOUT_SECONDS = 18.0
|
||||||
@@ -354,6 +357,11 @@ def event_question(event: dict[str, Any]) -> str:
|
|||||||
return bounded_text(match.group(1) if match else text, limit=MAX_EVENT_TEXT)
|
return bounded_text(match.group(1) if match else text, limit=MAX_EVENT_TEXT)
|
||||||
|
|
||||||
|
|
||||||
|
def is_ocr_event(event: dict[str, Any]) -> bool:
|
||||||
|
source = str(event.get("source", "")).casefold()
|
||||||
|
return source in OCR_EVENT_SOURCES or source.startswith("rapidocr-")
|
||||||
|
|
||||||
|
|
||||||
def classify_question(question: str) -> tuple[str, str]:
|
def classify_question(question: str) -> tuple[str, str]:
|
||||||
decision = route_question(question)
|
decision = route_question(question)
|
||||||
return decision.route.value, decision.reason
|
return decision.route.value, decision.reason
|
||||||
@@ -1293,6 +1301,43 @@ class Ledger:
|
|||||||
raise EdgeError("ledger write did not persist")
|
raise EdgeError("ledger write did not persist")
|
||||||
return result, cursor.rowcount == 1
|
return result, cursor.rowcount == 1
|
||||||
|
|
||||||
|
def recent_semantic_duplicate(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
group_fingerprint: str,
|
||||||
|
sender: str,
|
||||||
|
question: str,
|
||||||
|
route: str,
|
||||||
|
route_reason: str,
|
||||||
|
within_seconds: float = 8.0,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Find OCR re-ingestion of the same visible message under a new event id."""
|
||||||
|
group = bounded_text(group_fingerprint, limit=256)
|
||||||
|
actor = bounded_text(sender, limit=256) or "unknown"
|
||||||
|
content = bounded_text(question, limit=2_000)
|
||||||
|
if not group or not actor or not content:
|
||||||
|
return None
|
||||||
|
cutoff = (
|
||||||
|
datetime.now(timezone.utc) - timedelta(seconds=within_seconds)
|
||||||
|
).isoformat(timespec="milliseconds")
|
||||||
|
with self.lock:
|
||||||
|
row = self.connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM events
|
||||||
|
WHERE group_fingerprint = ?
|
||||||
|
AND sender = ?
|
||||||
|
AND question = ?
|
||||||
|
AND route = ?
|
||||||
|
AND route_reason = ?
|
||||||
|
AND created_at >= ?
|
||||||
|
ORDER BY created_at DESC, event_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(group, actor, content, route, route_reason, cutoff),
|
||||||
|
).fetchone()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
def set_event(
|
def set_event(
|
||||||
self,
|
self,
|
||||||
event_id: str,
|
event_id: str,
|
||||||
@@ -3140,7 +3185,7 @@ class Ledger:
|
|||||||
|
|
||||||
def _player_identity(self, group: str, sender: str) -> dict[str, str]:
|
def _player_identity(self, group: str, sender: str) -> dict[str, str]:
|
||||||
if not group or not sender or sender == "unknown":
|
if not group or not sender or sender == "unknown":
|
||||||
raise LocalCommandError("无法确认本人身份,不能操作金币账户")
|
raise LocalCommandError("无法确认本人身份,不能操作摩拉账户")
|
||||||
person = self.resolve_person(group, sender)
|
person = self.resolve_person(group, sender)
|
||||||
person["display_name"] = person_display_label(person["display_name"])
|
person["display_name"] = person_display_label(person["display_name"])
|
||||||
return person
|
return person
|
||||||
@@ -3178,13 +3223,15 @@ class Ledger:
|
|||||||
(group, person_key),
|
(group, person_key),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if stored is None:
|
if stored is None:
|
||||||
return normalize_player_account({})
|
account = normalize_player_account({})
|
||||||
|
append_coin_event(account, int(account["balance"]), "首次初始化")
|
||||||
|
return account
|
||||||
try:
|
try:
|
||||||
account = json.loads(str(stored["state_json"]))
|
account = json.loads(str(stored["state_json"]))
|
||||||
except json.JSONDecodeError as error:
|
except json.JSONDecodeError as error:
|
||||||
raise LocalCommandError("金币账户状态无效") from error
|
raise LocalCommandError("摩拉账户状态无效") from error
|
||||||
if not isinstance(account, dict):
|
if not isinstance(account, dict):
|
||||||
raise LocalCommandError("金币账户状态无效")
|
raise LocalCommandError("摩拉账户状态无效")
|
||||||
return normalize_player_account(account)
|
return normalize_player_account(account)
|
||||||
|
|
||||||
def _save_player_account(
|
def _save_player_account(
|
||||||
@@ -3199,7 +3246,7 @@ class Ledger:
|
|||||||
type(account["balance"]) is not int
|
type(account["balance"]) is not int
|
||||||
or not 0 <= account["balance"] <= 1_000_000_000
|
or not 0 <= account["balance"] <= 1_000_000_000
|
||||||
):
|
):
|
||||||
raise LocalCommandError("金币结算超出允许范围")
|
raise LocalCommandError("摩拉结算超出允许范围")
|
||||||
self.connection.execute(
|
self.connection.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO player_accounts VALUES (?, ?, ?, ?)
|
INSERT INTO player_accounts VALUES (?, ?, ?, ?)
|
||||||
@@ -3555,6 +3602,13 @@ class Ledger:
|
|||||||
+ " ".join(map(str, drawn_numbers))
|
+ " ".join(map(str, drawn_numbers))
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
append_game_coin_event(
|
||||||
|
account,
|
||||||
|
kind="lottery",
|
||||||
|
outcome=f"命中{matches}个",
|
||||||
|
stake=stake,
|
||||||
|
payout=payout,
|
||||||
|
)
|
||||||
self._save_player_account(
|
self._save_player_account(
|
||||||
group, key, account, timestamp
|
group, key, account, timestamp
|
||||||
)
|
)
|
||||||
@@ -3661,6 +3715,13 @@ class Ledger:
|
|||||||
payout=stake,
|
payout=stake,
|
||||||
summary="主持人取消,下注已全额退回",
|
summary="主持人取消,下注已全额退回",
|
||||||
)
|
)
|
||||||
|
append_game_coin_event(
|
||||||
|
account,
|
||||||
|
kind="lottery",
|
||||||
|
outcome="本轮取消",
|
||||||
|
stake=stake,
|
||||||
|
payout=stake,
|
||||||
|
)
|
||||||
self._save_player_account(
|
self._save_player_account(
|
||||||
group, key, account, timestamp
|
group, key, account, timestamp
|
||||||
)
|
)
|
||||||
@@ -3827,6 +3888,13 @@ class Ledger:
|
|||||||
f"玩家{player_value}点,Taffy{dealer_value}点"
|
f"玩家{player_value}点,Taffy{dealer_value}点"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
append_game_coin_event(
|
||||||
|
account,
|
||||||
|
kind="blackjack",
|
||||||
|
outcome=outcome,
|
||||||
|
stake=bet,
|
||||||
|
payout=payout,
|
||||||
|
)
|
||||||
self._save_player_account(group, key, account, timestamp)
|
self._save_player_account(group, key, account, timestamp)
|
||||||
self.connection.execute(
|
self.connection.execute(
|
||||||
"""
|
"""
|
||||||
@@ -4830,9 +4898,9 @@ class Ledger:
|
|||||||
try:
|
try:
|
||||||
account = json.loads(str(row["state_json"]))
|
account = json.loads(str(row["state_json"]))
|
||||||
except json.JSONDecodeError as error:
|
except json.JSONDecodeError as error:
|
||||||
raise LocalCommandError("金币账户状态无效") from error
|
raise LocalCommandError("摩拉账户状态无效") from error
|
||||||
if not isinstance(account, dict):
|
if not isinstance(account, dict):
|
||||||
raise LocalCommandError("金币账户状态无效")
|
raise LocalCommandError("摩拉账户状态无效")
|
||||||
normalize_player_account(account)
|
normalize_player_account(account)
|
||||||
loan = account.get("loan")
|
loan = account.get("loan")
|
||||||
loan_principal = (
|
loan_principal = (
|
||||||
@@ -4939,9 +5007,9 @@ class Ledger:
|
|||||||
try:
|
try:
|
||||||
account = json.loads(str(row["state_json"]))
|
account = json.loads(str(row["state_json"]))
|
||||||
except json.JSONDecodeError as error:
|
except json.JSONDecodeError as error:
|
||||||
raise LocalCommandError("金币账户状态无效") from error
|
raise LocalCommandError("摩拉账户状态无效") from error
|
||||||
if not isinstance(account, dict):
|
if not isinstance(account, dict):
|
||||||
raise LocalCommandError("金币账户状态无效")
|
raise LocalCommandError("摩拉账户状态无效")
|
||||||
normalize_player_account(account)
|
normalize_player_account(account)
|
||||||
stake = 0
|
stake = 0
|
||||||
payout = 0
|
payout = 0
|
||||||
@@ -5052,6 +5120,16 @@ class Ledger:
|
|||||||
)
|
)
|
||||||
source_account["balance"] -= amount
|
source_account["balance"] -= amount
|
||||||
target_account["balance"] += amount
|
target_account["balance"] += amount
|
||||||
|
append_coin_event(
|
||||||
|
source_account,
|
||||||
|
-amount,
|
||||||
|
f"转账给 {target_display_name}",
|
||||||
|
)
|
||||||
|
append_coin_event(
|
||||||
|
target_account,
|
||||||
|
amount,
|
||||||
|
f"转账 来自 {source['display_name']}",
|
||||||
|
)
|
||||||
timestamp = utc_now()
|
timestamp = utc_now()
|
||||||
self._save_player_account(
|
self._save_player_account(
|
||||||
group, source_key, source_account, timestamp
|
group, source_key, source_account, timestamp
|
||||||
@@ -6502,6 +6580,34 @@ class EdgeApplication:
|
|||||||
reason=reason,
|
reason=reason,
|
||||||
execution_path=execution_path,
|
execution_path=execution_path,
|
||||||
)
|
)
|
||||||
|
existing_row = self.ledger.row(event_id) if event_id else None
|
||||||
|
semantic_duplicate = None
|
||||||
|
if existing_row is None and is_ocr_event(event):
|
||||||
|
semantic_duplicate = self.ledger.recent_semantic_duplicate(
|
||||||
|
group_fingerprint=group_id,
|
||||||
|
sender=str(event.get("sender", "")),
|
||||||
|
question=question,
|
||||||
|
route=route,
|
||||||
|
route_reason=reason,
|
||||||
|
)
|
||||||
|
if semantic_duplicate is not None:
|
||||||
|
self.telemetry.elapsed(
|
||||||
|
"edge.event.accepted",
|
||||||
|
started,
|
||||||
|
status="semantic_duplicate",
|
||||||
|
event_id=event_id,
|
||||||
|
group_id=group_id,
|
||||||
|
route=route,
|
||||||
|
execution_path=execution_path,
|
||||||
|
duplicate=True,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": "ignored",
|
||||||
|
"route": route,
|
||||||
|
"execution_path": execution_path,
|
||||||
|
"duplicate": True,
|
||||||
|
"duplicate_event_id": str(semantic_duplicate["event_id"]),
|
||||||
|
}
|
||||||
row, created = self.ledger.record_event(
|
row, created = self.ledger.record_event(
|
||||||
event,
|
event,
|
||||||
question=question,
|
question=question,
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-official-extensions"
|
name = "jwebot-official-extensions"
|
||||||
version = "1.0.2b1+1016"
|
version = "1.0.3b1+1016"
|
||||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-sdk==1.0.2b1+1016",
|
"jwebot-sdk==1.0.3b1+1016",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
@@ -90,4 +90,4 @@ __all__ = [
|
|||||||
"render_feature_list",
|
"render_feature_list",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.2b1+1016"
|
__version__ = "1.0.3b1+1016"
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ PROFILE = 'coin-ranking'
|
|||||||
AUTHOR = 'jwebot'
|
AUTHOR = 'jwebot'
|
||||||
DESCRIPTION = '富榜'
|
DESCRIPTION = '富榜'
|
||||||
HELP_ENTRIES = ('/大富翁榜',)
|
HELP_ENTRIES = ('/大富翁榜',)
|
||||||
COMMANDS = ('/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
|
COMMANDS = ('/摩拉排行', '/摩拉排行榜', '/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
|
||||||
|
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
def _handler(context: SingleGroupContext, command: str):
|
||||||
|
|||||||
+3
-3
@@ -5,9 +5,9 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
|||||||
|
|
||||||
PROFILE = 'coin-taffy'
|
PROFILE = 'coin-taffy'
|
||||||
AUTHOR = 'jwebot'
|
AUTHOR = 'jwebot'
|
||||||
DESCRIPTION = 'Taffy金币'
|
DESCRIPTION = 'Taffy摩拉'
|
||||||
HELP_ENTRIES = ('/Taffy金币',)
|
HELP_ENTRIES = ('/Taffy摩拉(兼容 /Taffy金币)',)
|
||||||
COMMANDS = ('/Taffy金币', '/taffycoins')
|
COMMANDS = ('/Taffy摩拉', '/Taffy金币', '/taffycoins')
|
||||||
|
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
def _handler(context: SingleGroupContext, command: str):
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
|||||||
|
|
||||||
PROFILE = 'coin-transfer'
|
PROFILE = 'coin-transfer'
|
||||||
AUTHOR = 'jwebot'
|
AUTHOR = 'jwebot'
|
||||||
DESCRIPTION = '金币转账'
|
DESCRIPTION = '摩拉转账'
|
||||||
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
|
HELP_ENTRIES = ('/转账 对方 金额;/transfer',)
|
||||||
COMMANDS = ('/转账', '/transfer')
|
COMMANDS = ('/转账', '/transfer')
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -5,9 +5,9 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
|||||||
|
|
||||||
PROFILE = 'single-player-games'
|
PROFILE = 'single-player-games'
|
||||||
AUTHOR = 'jwebot'
|
AUTHOR = 'jwebot'
|
||||||
DESCRIPTION = '金币游戏'
|
DESCRIPTION = '摩拉游戏'
|
||||||
HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games')
|
HELP_ENTRIES = ('/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games')
|
||||||
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
COMMANDS = ('/摩拉', '/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games')
|
||||||
|
|
||||||
|
|
||||||
def _handler(context: SingleGroupContext, command: str):
|
def _handler(context: SingleGroupContext, command: str):
|
||||||
|
|||||||
+14
-14
@@ -956,7 +956,7 @@ def _shop_text(state: dict[str, Any], player: dict[str, Any]) -> str:
|
|||||||
lines = [
|
lines = [
|
||||||
(
|
(
|
||||||
f"{_player_prefix(state, player)}第 {state['round_number']} 回合"
|
f"{_player_prefix(state, player)}第 {state['round_number']} 回合"
|
||||||
f" {phase_name};金币 {player['gold']};生命 {player['hp']};"
|
f" {phase_name};摩拉 {player['gold']};生命 {player['hp']};"
|
||||||
f"教练 {player['coach_level']} 级;战术{player['tactic']};"
|
f"教练 {player['coach_level']} 级;战术{player['tactic']};"
|
||||||
f"阵容 {power['power']:.1f};商店{lock}"
|
f"阵容 {power['power']:.1f};商店{lock}"
|
||||||
)
|
)
|
||||||
@@ -1143,7 +1143,7 @@ def _auto_manage_ai(
|
|||||||
)
|
)
|
||||||
text = (
|
text = (
|
||||||
f"{player['display_name']} 自动运营:{'、'.join(actions)};"
|
f"{player['display_name']} 自动运营:{'、'.join(actions)};"
|
||||||
f"金币 {player['gold']},五人首发战力 "
|
f"摩拉 {player['gold']},五人首发战力 "
|
||||||
f"{lineup_power(player)['power']:.1f}。"
|
f"{lineup_power(player)['power']:.1f}。"
|
||||||
)
|
)
|
||||||
stories.append(
|
stories.append(
|
||||||
@@ -1571,7 +1571,7 @@ def _settle_pve(
|
|||||||
)
|
)
|
||||||
text = (
|
text = (
|
||||||
f"{player['display_name']} {'完成' if won else '未完成'}"
|
f"{player['display_name']} {'完成' if won else '未完成'}"
|
||||||
f"{event_name}目标 {adjusted:.1f}/{target},获得 {reward} 金币。"
|
f"{event_name}目标 {adjusted:.1f}/{target},获得 {reward} 摩拉。"
|
||||||
)
|
)
|
||||||
stories.append(_story(state, "pve_result", text, result))
|
stories.append(_story(state, "pve_result", text, result))
|
||||||
lines.append(_host_text(text))
|
lines.append(_host_text(text))
|
||||||
@@ -2326,7 +2326,7 @@ def _team_power_line(state: dict[str, Any], key: str) -> str:
|
|||||||
else f"{player.get('coach_level', 1)}级"
|
else f"{player.get('coach_level', 1)}级"
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
f"{player['display_name']} #{state['season_number']}:金币 {player['gold']};"
|
f"{player['display_name']} #{state['season_number']}:摩拉 {player['gold']};"
|
||||||
f"生命 {player['hp']};教练 {coach_text};战术{player['tactic']};"
|
f"生命 {player['hp']};教练 {coach_text};战术{player['tactic']};"
|
||||||
f"阵容 {power['power']:.1f};商店{'已锁' if player['shop_locked'] else '未锁'}"
|
f"阵容 {power['power']:.1f};商店{'已锁' if player['shop_locked'] else '未锁'}"
|
||||||
)
|
)
|
||||||
@@ -2417,7 +2417,7 @@ def _training_match_part(
|
|||||||
f"{_team_name(winner)} 以 {item['score']} 拿下本图。"
|
f"{_team_name(winner)} 以 {item['score']} 拿下本图。"
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
f"金币:{_team_name(winner)} +{TRAINING_WIN_GOLD},"
|
f"摩拉:{_team_name(winner)} +{TRAINING_WIN_GOLD},"
|
||||||
f"{_team_name(loser)} +{TRAINING_LOSS_GOLD}。"
|
f"{_team_name(loser)} +{TRAINING_LOSS_GOLD}。"
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -2433,7 +2433,7 @@ def _training_bye_part(state: dict[str, Any], key: str, round_number: int) -> st
|
|||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
f"训练赛第 {round_number} 轮:{_team_name(player)} 与本地陪练队完成热身。\n"
|
f"训练赛第 {round_number} 轮:{_team_name(player)} 与本地陪练队完成热身。\n"
|
||||||
f"{hook}\n金币:{_team_name(player)} +{TRAINING_BYE_GOLD}。"
|
f"{hook}\n摩拉:{_team_name(player)} +{TRAINING_BYE_GOLD}。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2461,7 +2461,7 @@ def _settle_training_round(
|
|||||||
if cursor == 0:
|
if cursor == 0:
|
||||||
intro = (
|
intro = (
|
||||||
f"欢迎来到{state['event_title']}!你们已经到达酒店宾馆,"
|
f"欢迎来到{state['event_title']}!你们已经到达酒店宾馆,"
|
||||||
f"训练赛共 {len(rounds)} 轮。训练赛不扣生命,只产出运营金币。"
|
f"训练赛共 {len(rounds)} 轮。训练赛不扣生命,只产出运营摩拉。"
|
||||||
)
|
)
|
||||||
parts.append(intro)
|
parts.append(intro)
|
||||||
stories.append(_story(state, "training_open", intro))
|
stories.append(_story(state, "training_open", intro))
|
||||||
@@ -2670,7 +2670,7 @@ def _series_report_parts(
|
|||||||
state["players"][winner_key]["gold"] = int(state["players"][winner_key]["gold"]) + reward
|
state["players"][winner_key]["gold"] = int(state["players"][winner_key]["gold"]) + reward
|
||||||
final_text = (
|
final_text = (
|
||||||
f"系列赛结束,{_team_name(state['players'][winner_key])} 以 {result['score']} 击败 "
|
f"系列赛结束,{_team_name(state['players'][winner_key])} 以 {result['score']} 击败 "
|
||||||
f"{_team_name(state['players'][loser_key])},获得 {reward} 金币。"
|
f"{_team_name(state['players'][loser_key])},获得 {reward} 摩拉。"
|
||||||
"这笔钱可以马上用于 /刷新、/选1~/选6 或锁店等下一轮运营。"
|
"这笔钱可以马上用于 /刷新、/选1~/选6 或锁店等下一轮运营。"
|
||||||
)
|
)
|
||||||
parts[-1] = parts[-1] + "\n" + final_text
|
parts[-1] = parts[-1] + "\n" + final_text
|
||||||
@@ -2745,7 +2745,7 @@ def _special_event_parts(
|
|||||||
)
|
)
|
||||||
text = (
|
text = (
|
||||||
f"{_team_name(player)} {'完成' if won else '勉强应付'}{event_kind}:"
|
f"{_team_name(player)} {'完成' if won else '勉强应付'}{event_kind}:"
|
||||||
f"{hook}获得 {reward} 金币。"
|
f"{hook}获得 {reward} 摩拉。"
|
||||||
)
|
)
|
||||||
parts.append(text)
|
parts.append(text)
|
||||||
stories.append(_story(state, "special_event", text, {"person_key": key}))
|
stories.append(_story(state, "special_event", text, {"person_key": key}))
|
||||||
@@ -3141,7 +3141,7 @@ class CsMajorAutochess:
|
|||||||
story = _story(state, "tactic_selected", text, {"person_key": person_key, "tactic": tactic})
|
story = _story(state, "tactic_selected", text, {"person_key": person_key, "tactic": tactic})
|
||||||
return respond(state, text, stories=[story])
|
return respond(state, text, stories=[story])
|
||||||
if action == "coach":
|
if action == "coach":
|
||||||
return respond(state, "教练现在由第一轮选秀决定,本届不能用金币升级教练。")
|
return respond(state, "教练现在由第一轮选秀决定,本届不能用摩拉升级教练。")
|
||||||
if action == "buy":
|
if action == "buy":
|
||||||
values = _parse_indices(arguments)
|
values = _parse_indices(arguments)
|
||||||
if len(values) != 1 or not 1 <= values[0] <= SHOP_SIZE:
|
if len(values) != 1 or not 1 <= values[0] <= SHOP_SIZE:
|
||||||
@@ -3152,7 +3152,7 @@ class CsMajorAutochess:
|
|||||||
return respond(state, "这个商店位置已经购买或没有候选")
|
return respond(state, "这个商店位置已经购买或没有候选")
|
||||||
card = CARD_BY_ID[str(card_id)]
|
card = CARD_BY_ID[str(card_id)]
|
||||||
if int(player["gold"]) < int(card["cost"]):
|
if int(player["gold"]) < int(card["cost"]):
|
||||||
return respond(state, f"金币不足:{card['name']} 需要 {card['cost']},当前 {player['gold']}")
|
return respond(state, f"摩拉不足:{card['name']} 需要 {card['cost']},当前 {player['gold']}")
|
||||||
existing = any(
|
existing = any(
|
||||||
unit["card_id"] == card_id
|
unit["card_id"] == card_id
|
||||||
for unit in [*player["board"], *player["bench"]]
|
for unit in [*player["board"], *player["bench"]]
|
||||||
@@ -3166,7 +3166,7 @@ class CsMajorAutochess:
|
|||||||
star, upgraded = _merge_purchase(player, str(card_id))
|
star, upgraded = _merge_purchase(player, str(card_id))
|
||||||
player["shop"][slot] = None
|
player["shop"][slot] = None
|
||||||
text = (
|
text = (
|
||||||
f"{display_name} 签下 {card['name']},花费 {card['cost']} 金币;"
|
f"{display_name} 签下 {card['name']},花费 {card['cost']} 摩拉;"
|
||||||
f"当前 {star} 星,余额 {player['gold']}。"
|
f"当前 {star} 星,余额 {player['gold']}。"
|
||||||
)
|
)
|
||||||
if upgraded:
|
if upgraded:
|
||||||
@@ -3175,7 +3175,7 @@ class CsMajorAutochess:
|
|||||||
return respond(state, text, stories=[story])
|
return respond(state, text, stories=[story])
|
||||||
if action == "refresh":
|
if action == "refresh":
|
||||||
if int(player["gold"]) < REFRESH_COST:
|
if int(player["gold"]) < REFRESH_COST:
|
||||||
return respond(state, f"刷新需要 {REFRESH_COST} 金币,当前 {player['gold']}")
|
return respond(state, f"刷新需要 {REFRESH_COST} 摩拉,当前 {player['gold']}")
|
||||||
player["gold"] -= REFRESH_COST
|
player["gold"] -= REFRESH_COST
|
||||||
player["shop"] = _draw_shop(
|
player["shop"] = _draw_shop(
|
||||||
state,
|
state,
|
||||||
@@ -3222,7 +3222,7 @@ class CsMajorAutochess:
|
|||||||
refund = int(card["cost"]) * int(unit["copies"])
|
refund = int(card["cost"]) * int(unit["copies"])
|
||||||
player["gold"] += refund
|
player["gold"] += refund
|
||||||
state["pool"][card["id"]] += int(unit["copies"])
|
state["pool"][card["id"]] += int(unit["copies"])
|
||||||
return respond(state, f"{display_name} 出售 {card['name']},返还 {refund} 金币。")
|
return respond(state, f"{display_name} 出售 {card['name']},返还 {refund} 摩拉。")
|
||||||
|
|
||||||
if action == "ready":
|
if action == "ready":
|
||||||
if person_key not in state.get("ready_keys", []):
|
if person_key not in state.get("ready_keys", []):
|
||||||
|
|||||||
+4
-4
@@ -11,7 +11,7 @@
|
|||||||
"队伍如果阵容完整,就更容易把这次插曲处理成奖金。",
|
"队伍如果阵容完整,就更容易把这次插曲处理成奖金。",
|
||||||
"这次挑战只出现一次,处理完就回到小组赛主线。",
|
"这次挑战只出现一次,处理完就回到小组赛主线。",
|
||||||
"主舞台彩排占用了热身时间,教练的管理能力开始生效。",
|
"主舞台彩排占用了热身时间,教练的管理能力开始生效。",
|
||||||
"赞助商要求展示新战术,选错方向也不会淘汰,只会少拿金币。",
|
"赞助商要求展示新战术,选错方向也不会淘汰,只会少拿摩拉。",
|
||||||
"现场采访提到了前一场失利,队员心态需要稳住。",
|
"现场采访提到了前一场失利,队员心态需要稳住。",
|
||||||
"这类挑战不扣生命,目的只是给运营阶段增加变数。",
|
"这类挑战不扣生命,目的只是给运营阶段增加变数。",
|
||||||
"处理好这次插曲的队伍,会在下一场前多一点经济余地。"
|
"处理好这次插曲的队伍,会在下一场前多一点经济余地。"
|
||||||
@@ -141,21 +141,21 @@
|
|||||||
"{team} 这波 {count} 连分让地图气质变了,所有枪位都开始往前压。"
|
"{team} 这波 {count} 连分让地图气质变了,所有枪位都开始往前压。"
|
||||||
],
|
],
|
||||||
"training_hooks": [
|
"training_hooks": [
|
||||||
"训练赛不计入小组胜负,但会发金币,方便后续刷新商店。",
|
"训练赛不计入小组胜负,但会发摩拉,方便后续刷新商店。",
|
||||||
"这轮训练更像实战热身,每队都会遇到同场参赛对手。",
|
"这轮训练更像实战热身,每队都会遇到同场参赛对手。",
|
||||||
"训练赛重点不是淘汰谁,而是检验当前阵容能不能打完整张图。",
|
"训练赛重点不是淘汰谁,而是检验当前阵容能不能打完整张图。",
|
||||||
"这轮结束后可以 D 牌、锁店或调首发,下一轮再检验成果。",
|
"这轮结束后可以 D 牌、锁店或调首发,下一轮再检验成果。",
|
||||||
"AI 战队也会参与训练,对局结果会写入档案。",
|
"AI 战队也会参与训练,对局结果会写入档案。",
|
||||||
"如果阵容缺少指挥,训练赛的中期回合会更容易失控。",
|
"如果阵容缺少指挥,训练赛的中期回合会更容易失控。",
|
||||||
"如果你想追羁绊,现在是最适合刷新商店的窗口。",
|
"如果你想追羁绊,现在是最适合刷新商店的窗口。",
|
||||||
"训练赛的金币很重要,别把每次刷新都当成免费试错。",
|
"训练赛的摩拉很重要,别把每次刷新都当成免费试错。",
|
||||||
"这一阶段没有生命扣除,重点是运营和阵容成型。",
|
"这一阶段没有生命扣除,重点是运营和阵容成型。",
|
||||||
"训练赛的地图会轮换,不会一直用同一个固定比分模板。",
|
"训练赛的地图会轮换,不会一直用同一个固定比分模板。",
|
||||||
"每轮训练结束后,真人都需要发送 /结束回合 才会进入下一轮。",
|
"每轮训练结束后,真人都需要发送 /结束回合 才会进入下一轮。",
|
||||||
"训练赛会按轮次播报,避免一次性刷出一大串结果。",
|
"训练赛会按轮次播报,避免一次性刷出一大串结果。",
|
||||||
"如果出现轮空,会安排本地陪练队补一张 BO1。",
|
"如果出现轮空,会安排本地陪练队补一张 BO1。",
|
||||||
"训练阶段适合观察战术克制,火力、稳定、残局都会影响回合走向。",
|
"训练阶段适合观察战术克制,火力、稳定、残局都会影响回合走向。",
|
||||||
"连胜会带来金币,但失败也有保底,保证测试局能继续运营。",
|
"连胜会带来摩拉,但失败也有保底,保证测试局能继续运营。",
|
||||||
"这部分是纯文字自走棋的节奏层,不再是单条目标数值判定。"
|
"这部分是纯文字自走棋的节奏层,不再是单条目标数值判定。"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,12 +40,16 @@ LOCAL_COMMAND_ALIASES = {
|
|||||||
"steam": "steam_friends",
|
"steam": "steam_friends",
|
||||||
"上下文": "context_summary",
|
"上下文": "context_summary",
|
||||||
"context": "context_summary",
|
"context": "context_summary",
|
||||||
|
"摩拉": "wallet",
|
||||||
"金币": "wallet",
|
"金币": "wallet",
|
||||||
"wallet": "wallet",
|
"wallet": "wallet",
|
||||||
|
"摩拉排行": "coin_ranking",
|
||||||
|
"摩拉排行榜": "coin_ranking",
|
||||||
"金币排行": "coin_ranking",
|
"金币排行": "coin_ranking",
|
||||||
"金币排行榜": "coin_ranking",
|
"金币排行榜": "coin_ranking",
|
||||||
"大富翁榜": "coin_ranking",
|
"大富翁榜": "coin_ranking",
|
||||||
"richlist": "coin_ranking",
|
"richlist": "coin_ranking",
|
||||||
|
"Taffy摩拉": "taffy_coins",
|
||||||
"Taffy金币": "taffy_coins",
|
"Taffy金币": "taffy_coins",
|
||||||
"taffycoins": "taffy_coins",
|
"taffycoins": "taffy_coins",
|
||||||
"转账": "transfer",
|
"转账": "transfer",
|
||||||
@@ -152,9 +156,9 @@ LOCAL_EXTENSIONS = (
|
|||||||
LocalExtensionDescriptor(
|
LocalExtensionDescriptor(
|
||||||
extension_id="single-player-games",
|
extension_id="single-player-games",
|
||||||
author=JWEBOT_AUTHOR,
|
author=JWEBOT_AUTHOR,
|
||||||
description="金币游戏",
|
description="摩拉游戏",
|
||||||
help_entries=(
|
help_entries=(
|
||||||
"/金币 /wallet;/游戏记录 /gamehistory",
|
"/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory",
|
||||||
"/抽奖 [金额];/draw",
|
"/抽奖 [金额];/draw",
|
||||||
"/彩票 开始|报名|状态|开奖|取消;/lottery",
|
"/彩票 开始|报名|状态|开奖|取消;/lottery",
|
||||||
"/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200",
|
"/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200",
|
||||||
@@ -172,7 +176,7 @@ LOCAL_EXTENSIONS = (
|
|||||||
LocalExtensionDescriptor(
|
LocalExtensionDescriptor(
|
||||||
extension_id="coin-transfer",
|
extension_id="coin-transfer",
|
||||||
author=JWEBOT_AUTHOR,
|
author=JWEBOT_AUTHOR,
|
||||||
description="金币转账",
|
description="摩拉转账",
|
||||||
help_entries=("/转账 对方 金额;/transfer",),
|
help_entries=("/转账 对方 金额;/transfer",),
|
||||||
command_kinds=("transfer",),
|
command_kinds=("transfer",),
|
||||||
),
|
),
|
||||||
@@ -186,8 +190,8 @@ LOCAL_EXTENSIONS = (
|
|||||||
LocalExtensionDescriptor(
|
LocalExtensionDescriptor(
|
||||||
extension_id="coin-taffy",
|
extension_id="coin-taffy",
|
||||||
author=JWEBOT_AUTHOR,
|
author=JWEBOT_AUTHOR,
|
||||||
description="Taffy金币",
|
description="Taffy摩拉",
|
||||||
help_entries=("/Taffy金币",),
|
help_entries=("/Taffy摩拉(兼容 /Taffy金币)",),
|
||||||
command_kinds=("taffy_coins",),
|
command_kinds=("taffy_coins",),
|
||||||
),
|
),
|
||||||
LocalExtensionDescriptor(
|
LocalExtensionDescriptor(
|
||||||
|
|||||||
+1
-1
@@ -365,7 +365,7 @@ class LocalCommandEngine:
|
|||||||
return (f"人物:{self.person_display_label(person['display_name'])};"
|
return (f"人物:{self.person_display_label(person['display_name'])};"
|
||||||
f"群内索引 {person['person_key'][:12]};别名:{aliases or '未配置'}。\n"
|
f"群内索引 {person['person_key'][:12]};别名:{aliases or '未配置'}。\n"
|
||||||
f"基本属性:{facts or '暂无配置'}。\n"
|
f"基本属性:{facts or '暂无配置'}。\n"
|
||||||
"本人金币 /金币;个人记忆 /我的记忆。身份由本地昵称映射解析,无法读取微信原生账号ID。")
|
"本人摩拉 /摩拉;个人记忆 /我的记忆。身份由本地昵称映射解析,无法读取微信原生账号ID。")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def service_state(service_name: str) -> str:
|
def service_state(service_name: str) -> str:
|
||||||
|
|||||||
+194
-54
@@ -26,6 +26,8 @@ MAX_LOAN_AMOUNT = 500
|
|||||||
GAME_TIMEOUT_SECONDS = 600
|
GAME_TIMEOUT_SECONDS = 600
|
||||||
GAME_HISTORY_LIMIT = 50
|
GAME_HISTORY_LIMIT = 50
|
||||||
GAME_HISTORY_PAGE_SIZE = 5
|
GAME_HISTORY_PAGE_SIZE = 5
|
||||||
|
COIN_EVENT_LIMIT = 50
|
||||||
|
COIN_EVENT_DISPLAY_LIMIT = 10
|
||||||
GAME_LABELS = {
|
GAME_LABELS = {
|
||||||
"prize_draw": "抽奖",
|
"prize_draw": "抽奖",
|
||||||
"lottery": "彩票",
|
"lottery": "彩票",
|
||||||
@@ -54,11 +56,48 @@ BLACKJACK_TABLE_BET = re.compile(
|
|||||||
)
|
)
|
||||||
TRANSFER_MARKED = re.compile(
|
TRANSFER_MARKED = re.compile(
|
||||||
r"^(?:给|to)?\s*(?P<target>.+?)\s*"
|
r"^(?:给|to)?\s*(?P<target>.+?)\s*"
|
||||||
r"(?:金额|金币|coin|coins|:|:)\s*(?P<amount>\d{1,6})$",
|
r"(?:金额|摩拉|金币|coin|coins|:|:)\s*(?P<amount>\d{1,6})$",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def coin_event_reason(kind: str, outcome: str) -> str:
|
||||||
|
label = GAME_LABELS.get(kind, kind)
|
||||||
|
if kind == "blackjack":
|
||||||
|
if "赢" in outcome or "获胜" in outcome:
|
||||||
|
return f"{label} 获胜"
|
||||||
|
if "输" in outcome or "认输" in outcome or "爆" in outcome:
|
||||||
|
return f"{label} 失败"
|
||||||
|
if "平" in outcome:
|
||||||
|
return f"{label} 平局"
|
||||||
|
return f"{label} {outcome}".strip()
|
||||||
|
|
||||||
|
|
||||||
|
def append_coin_event(
|
||||||
|
account: dict[str, Any],
|
||||||
|
delta: int,
|
||||||
|
reason: str,
|
||||||
|
) -> None:
|
||||||
|
if delta == 0:
|
||||||
|
return
|
||||||
|
events = account.get("coin_events")
|
||||||
|
if not isinstance(events, list):
|
||||||
|
events = []
|
||||||
|
events = [
|
||||||
|
event for event in events
|
||||||
|
if isinstance(event, dict) and type(event.get("delta")) is int
|
||||||
|
]
|
||||||
|
events.append(
|
||||||
|
{
|
||||||
|
"delta": delta,
|
||||||
|
"reason": str(reason)[:120],
|
||||||
|
"balance": int(account.get("balance", 0)),
|
||||||
|
"created_at": time.time(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
account["coin_events"] = events[-COIN_EVENT_LIMIT:]
|
||||||
|
|
||||||
|
|
||||||
def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||||
account.setdefault("balance", INITIAL_COINS)
|
account.setdefault("balance", INITIAL_COINS)
|
||||||
account.setdefault("game", None)
|
account.setdefault("game", None)
|
||||||
@@ -90,6 +129,33 @@ def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
|||||||
account["game_history"] = [
|
account["game_history"] = [
|
||||||
entry for entry in history if isinstance(entry, dict)
|
entry for entry in history if isinstance(entry, dict)
|
||||||
][-GAME_HISTORY_LIMIT:]
|
][-GAME_HISTORY_LIMIT:]
|
||||||
|
events = account.get("coin_events")
|
||||||
|
if not isinstance(events, list):
|
||||||
|
events = []
|
||||||
|
for entry in account["game_history"]:
|
||||||
|
try:
|
||||||
|
stake = int(entry.get("stake", 0))
|
||||||
|
payout = int(entry.get("payout", 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
delta = payout - stake
|
||||||
|
if delta == 0:
|
||||||
|
continue
|
||||||
|
events.append(
|
||||||
|
{
|
||||||
|
"delta": delta,
|
||||||
|
"reason": coin_event_reason(
|
||||||
|
str(entry.get("kind", "")),
|
||||||
|
str(entry.get("outcome", "")),
|
||||||
|
),
|
||||||
|
"balance": int(account["balance"]),
|
||||||
|
"created_at": float(entry.get("completed_at", 0) or 0),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
account["coin_events"] = [
|
||||||
|
event for event in events
|
||||||
|
if isinstance(event, dict) and type(event.get("delta")) is int
|
||||||
|
][-COIN_EVENT_LIMIT:]
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
@@ -133,6 +199,40 @@ def append_game_record(
|
|||||||
account["game_history"] = history[-GAME_HISTORY_LIMIT:]
|
account["game_history"] = history[-GAME_HISTORY_LIMIT:]
|
||||||
|
|
||||||
|
|
||||||
|
def append_game_coin_event(
|
||||||
|
account: dict[str, Any],
|
||||||
|
*,
|
||||||
|
kind: str,
|
||||||
|
outcome: str,
|
||||||
|
stake: int,
|
||||||
|
payout: int,
|
||||||
|
) -> None:
|
||||||
|
append_coin_event(
|
||||||
|
account,
|
||||||
|
payout - stake,
|
||||||
|
coin_event_reason(kind, outcome),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def recent_coin_event_lines(
|
||||||
|
account: dict[str, Any],
|
||||||
|
*,
|
||||||
|
limit: int = COIN_EVENT_DISPLAY_LIMIT,
|
||||||
|
) -> list[str]:
|
||||||
|
normalize_player_account(account)
|
||||||
|
events = sorted(
|
||||||
|
account["coin_events"],
|
||||||
|
key=lambda event: float(event.get("created_at", 0) or 0),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
lines: list[str] = []
|
||||||
|
for event in events[:limit]:
|
||||||
|
delta = int(event["delta"])
|
||||||
|
reason = str(event.get("reason", "账户变动")).strip() or "账户变动"
|
||||||
|
lines.append(f"{delta:+d}:{reason}")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def card_label(card: int) -> str:
|
def card_label(card: int) -> str:
|
||||||
return ("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K")[card % 13]
|
return ("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K")[card % 13]
|
||||||
|
|
||||||
@@ -155,10 +255,11 @@ class PlayerGames:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def rules() -> str:
|
def rules() -> str:
|
||||||
return (
|
return (
|
||||||
"每人首次使用获 1000 虚拟金币;/金币 查看余额;/游戏记录 查看本人记录。\n"
|
"每人首次使用获 1000 虚拟摩拉;/摩拉 查看余额(兼容 /金币);"
|
||||||
|
"/游戏记录 查看本人记录。\n"
|
||||||
"转账:/转账 对方 金额,或 /transfer name amount;"
|
"转账:/转账 对方 金额,或 /transfer name amount;"
|
||||||
"对方需先用 /金币 初始化账户。\n"
|
"对方需先用 /摩拉 或 /金币 初始化账户。\n"
|
||||||
"贷款:/贷款 [1~500] 借入金币;/还款 [金额|全部] 归还,"
|
"贷款:/贷款 [1~500] 借入摩拉;/还款 [金额|全部] 归还,"
|
||||||
"同一时间最多一笔未还贷款。\n"
|
"同一时间最多一笔未还贷款。\n"
|
||||||
"抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n"
|
"抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n"
|
||||||
"多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];"
|
"多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];"
|
||||||
@@ -172,7 +273,7 @@ class PlayerGames:
|
|||||||
"主持人 /21点 多人 开始 后按提示 /21点 放200"
|
"主持人 /21点 多人 开始 后按提示 /21点 放200"
|
||||||
"(1~10000);轮到自己 /21点 要牌|停牌。\n"
|
"(1~10000);轮到自己 /21点 要牌|停牌。\n"
|
||||||
"21点获胜返2倍、平局返下注、自然21点返2.5倍向下取整,庄家软17停牌。"
|
"21点获胜返2倍、平局返下注、自然21点返2.5倍向下取整,庄家软17停牌。"
|
||||||
"以上均含本金;虚拟金币不能充值或兑换。"
|
"以上均含本金;虚拟摩拉不能充值或兑换。"
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, row: dict[str, Any], kind: str, arguments: str) -> str:
|
def handle(self, row: dict[str, Any], kind: str, arguments: str) -> str:
|
||||||
@@ -194,10 +295,10 @@ class PlayerGames:
|
|||||||
if kind in {"loan", "repay"}:
|
if kind in {"loan", "repay"}:
|
||||||
return self.loan_or_repay(row, kind, arguments)
|
return self.loan_or_repay(row, kind, arguments)
|
||||||
if kind == "wallet" and arguments:
|
if kind == "wallet" and arguments:
|
||||||
return "用法:/金币(/wallet),查看本人的虚拟金币;/游戏帮助 查看规则"
|
return "用法:/摩拉(兼容 /金币、/wallet),查看本人的虚拟摩拉;/游戏帮助 查看规则"
|
||||||
if kind == "prize_draw":
|
if kind == "prize_draw":
|
||||||
if arguments and not re.fullmatch(r"\d{1,3}", arguments):
|
if arguments and not re.fullmatch(r"\d{1,3}", arguments):
|
||||||
return "用法:/抽奖 [2~100金币](/draw),默认10金币"
|
return "用法:/抽奖 [2~100摩拉](/draw),默认10摩拉"
|
||||||
bet = int(arguments or 10)
|
bet = int(arguments or 10)
|
||||||
elif kind == "blackjack":
|
elif kind == "blackjack":
|
||||||
table_command = self.parse_blackjack_table_command(arguments)
|
table_command = self.parse_blackjack_table_command(arguments)
|
||||||
@@ -255,13 +356,13 @@ class PlayerGames:
|
|||||||
else:
|
else:
|
||||||
bet = 0
|
bet = 0
|
||||||
if kind == "prize_draw" and not MIN_DRAW_BET <= bet <= MAX_DRAW_BET:
|
if kind == "prize_draw" and not MIN_DRAW_BET <= bet <= MAX_DRAW_BET:
|
||||||
return "抽奖下注必须为 2~100 的整数金币"
|
return "抽奖下注必须为 2~100 的整数摩拉"
|
||||||
if (
|
if (
|
||||||
kind == "blackjack"
|
kind == "blackjack"
|
||||||
and action == "start"
|
and action == "start"
|
||||||
and not MIN_BLACKJACK_BET <= bet <= MAX_BLACKJACK_BET
|
and not MIN_BLACKJACK_BET <= bet <= MAX_BLACKJACK_BET
|
||||||
):
|
):
|
||||||
return "21点下注必须为 1~500 的整数金币"
|
return "21点下注必须为 1~500 的整数摩拉"
|
||||||
person = self.ledger.resolve_person(
|
person = self.ledger.resolve_person(
|
||||||
str(row["group_fingerprint"]),
|
str(row["group_fingerprint"]),
|
||||||
str(row["sender"]),
|
str(row["sender"]),
|
||||||
@@ -291,6 +392,13 @@ class PlayerGames:
|
|||||||
payout=0,
|
payout=0,
|
||||||
summary="10分钟无有效操作,按认输结束",
|
summary="10分钟无有效操作,按认输结束",
|
||||||
)
|
)
|
||||||
|
append_game_coin_event(
|
||||||
|
account,
|
||||||
|
kind="blackjack",
|
||||||
|
outcome="超时认输",
|
||||||
|
stake=int(game["bet"]),
|
||||||
|
payout=0,
|
||||||
|
)
|
||||||
account["game"] = None
|
account["game"] = None
|
||||||
account["losses"] += 1
|
account["losses"] += 1
|
||||||
expired = "上一局已超时,按认输结束;"
|
expired = "上一局已超时,按认输结束;"
|
||||||
@@ -298,11 +406,20 @@ class PlayerGames:
|
|||||||
state = "进行中" if account.get("game") else "无进行中的牌局"
|
state = "进行中" if account.get("game") else "无进行中的牌局"
|
||||||
loan = account.get("loan")
|
loan = account.get("loan")
|
||||||
loan_text = f"未还贷款 {loan['principal']};" if loan else "无贷款;"
|
loan_text = f"未还贷款 {loan['principal']};" if loan else "无贷款;"
|
||||||
return (
|
total = account["wins"] + account["losses"] + account["ties"]
|
||||||
f"{expired}金币 {account['balance']};{loan_text}21点 {state};"
|
win_rate = (account["wins"] / total * 100) if total else 0.0
|
||||||
f"已结算胜 {account['wins']} / 负 {account['losses']} / 平 {account['ties']}。"
|
lines = [
|
||||||
"首次赠送1000仅发一次;/游戏帮助 查看规则"
|
f"{player_label}:{expired}当前摩拉 {account['balance']};{loan_text}",
|
||||||
)
|
(
|
||||||
|
f"21点 {state};已结算胜 {account['wins']} / "
|
||||||
|
f"负 {account['losses']} / 平 {account['ties']},"
|
||||||
|
f"胜率 {win_rate:.2f}%。"
|
||||||
|
),
|
||||||
|
"最近10次账户变动:",
|
||||||
|
]
|
||||||
|
lines.extend(recent_coin_event_lines(account) or ["暂无"])
|
||||||
|
lines.append("/游戏帮助 查看规则")
|
||||||
|
return "\n".join(lines)
|
||||||
if kind == "blackjack" and action in {"hit", "stand", "status", "end"}:
|
if kind == "blackjack" and action in {"hit", "stand", "status", "end"}:
|
||||||
active = account.get("game")
|
active = account.get("game")
|
||||||
index = int(active["index"]) if active else None
|
index = int(active["index"]) if active else None
|
||||||
@@ -318,7 +435,7 @@ class PlayerGames:
|
|||||||
else message
|
else message
|
||||||
)
|
)
|
||||||
if account["balance"] < bet:
|
if account["balance"] < bet:
|
||||||
message = expired + f"金币不足:余额 {account['balance']},本次需要 {bet}"
|
message = expired + f"摩拉不足:余额 {account['balance']},本次需要 {bet}"
|
||||||
return player_reply(message) if kind == "blackjack" else message
|
return player_reply(message) if kind == "blackjack" else message
|
||||||
account["balance"] -= bet
|
account["balance"] -= bet
|
||||||
if kind == "blackjack":
|
if kind == "blackjack":
|
||||||
@@ -350,10 +467,17 @@ class PlayerGames:
|
|||||||
payout=payout,
|
payout=payout,
|
||||||
summary=f"抽奖结果 {result}",
|
summary=f"抽奖结果 {result}",
|
||||||
)
|
)
|
||||||
|
append_game_coin_event(
|
||||||
|
account,
|
||||||
|
kind="prize_draw",
|
||||||
|
outcome=result,
|
||||||
|
stake=bet,
|
||||||
|
payout=payout,
|
||||||
|
)
|
||||||
return player_reply(
|
return player_reply(
|
||||||
expired
|
expired
|
||||||
+ f"{result};下注 {bet},返还 {payout},"
|
+ f"{result};下注 {bet},返还 {payout},"
|
||||||
f"净变化 {payout - bet:+d};金币 {account['balance']}",
|
f"净变化 {payout - bet:+d};摩拉 {account['balance']}",
|
||||||
index,
|
index,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -364,22 +488,22 @@ class PlayerGames:
|
|||||||
def coin_ranking(self, row: dict[str, Any], arguments: str) -> str:
|
def coin_ranking(self, row: dict[str, Any], arguments: str) -> str:
|
||||||
value = arguments.strip()
|
value = arguments.strip()
|
||||||
if value and not re.fullmatch(r"\d{1,2}", value):
|
if value and not re.fullmatch(r"\d{1,2}", value):
|
||||||
return "用法:/大富翁榜 或 /金币排行 [人数]"
|
return "用法:/大富翁榜、/摩拉排行 [人数] 或 /金币排行 [人数]"
|
||||||
limit = int(value or 10)
|
limit = int(value or 10)
|
||||||
if not 1 <= limit <= 20:
|
if not 1 <= limit <= 20:
|
||||||
return "金币排行人数必须为 1~20"
|
return "摩拉排行人数必须为 1~20"
|
||||||
records = self.ledger.player_coin_ranking(
|
records = self.ledger.player_coin_ranking(
|
||||||
str(row["group_fingerprint"]),
|
str(row["group_fingerprint"]),
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
if not records:
|
if not records:
|
||||||
return "当前群还没有金币账户;发送 /金币 初始化"
|
return "当前群还没有摩拉账户;发送 /摩拉 或 /金币 初始化"
|
||||||
lines = ["大富翁榜:"]
|
lines = ["大富翁榜:"]
|
||||||
for index, record in enumerate(records, start=1):
|
for index, record in enumerate(records, start=1):
|
||||||
loan = int(record.get("loan", 0))
|
loan = int(record.get("loan", 0))
|
||||||
loan_text = f"(未还贷款 {loan})" if loan else ""
|
loan_text = f"(未还贷款 {loan})" if loan else ""
|
||||||
lines.append(
|
lines.append(
|
||||||
f"{index}. {record['display_name']} {record['balance']} 金币"
|
f"{index}. {record['display_name']} {record['balance']} 摩拉"
|
||||||
f"{loan_text}"
|
f"{loan_text}"
|
||||||
)
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
@@ -387,21 +511,21 @@ class PlayerGames:
|
|||||||
def taffy_coins(self, row: dict[str, Any], arguments: str) -> str:
|
def taffy_coins(self, row: dict[str, Any], arguments: str) -> str:
|
||||||
value = arguments.strip()
|
value = arguments.strip()
|
||||||
if value and not re.fullmatch(r"\d{1,2}", value):
|
if value and not re.fullmatch(r"\d{1,2}", value):
|
||||||
return "用法:/Taffy金币 [人数]"
|
return "用法:/Taffy摩拉 [人数](兼容 /Taffy金币)"
|
||||||
limit = int(value or 8)
|
limit = int(value or 8)
|
||||||
if not 1 <= limit <= 20:
|
if not 1 <= limit <= 20:
|
||||||
return "Taffy金币人数必须为 1~20"
|
return "Taffy摩拉人数必须为 1~20"
|
||||||
stats = self.ledger.player_taffy_coin_stats(
|
stats = self.ledger.player_taffy_coin_stats(
|
||||||
str(row["group_fingerprint"]),
|
str(row["group_fingerprint"]),
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
if int(stats["records"]) == 0:
|
if int(stats["records"]) == 0:
|
||||||
return "Taffy金币:当前群暂无已结算游戏记录"
|
return "Taffy摩拉:当前群暂无已结算游戏记录"
|
||||||
net = int(stats["taffy_net"])
|
net = int(stats["taffy_net"])
|
||||||
result = f"Taffy累计赢 {net} 金币" if net >= 0 else f"Taffy累计倒贴 {-net} 金币"
|
result = f"Taffy累计赢 {net} 摩拉" if net >= 0 else f"Taffy累计倒贴 {-net} 摩拉"
|
||||||
lines = [
|
lines = [
|
||||||
(
|
(
|
||||||
f"Taffy金币:已结算 {stats['records']} 局;"
|
f"Taffy摩拉:已结算 {stats['records']} 局;"
|
||||||
f"玩家总下注 {stats['stake']},总返还 {stats['payout']};"
|
f"玩家总下注 {stats['stake']},总返还 {stats['payout']};"
|
||||||
f"{result}"
|
f"{result}"
|
||||||
)
|
)
|
||||||
@@ -551,7 +675,7 @@ class PlayerGames:
|
|||||||
)
|
)
|
||||||
if action == "bet":
|
if action == "bet":
|
||||||
if not MIN_BLACKJACK_TABLE_BET <= amount <= MAX_BLACKJACK_TABLE_BET:
|
if not MIN_BLACKJACK_TABLE_BET <= amount <= MAX_BLACKJACK_TABLE_BET:
|
||||||
return "21点多人牌桌筹码必须为 1~10000 的整数金币"
|
return "21点多人牌桌筹码必须为 1~10000 的整数摩拉"
|
||||||
deck = list(range(52))
|
deck = list(range(52))
|
||||||
self.rng.shuffle(deck)
|
self.rng.shuffle(deck)
|
||||||
return self.ledger.place_group_blackjack_bet(
|
return self.ledger.place_group_blackjack_bet(
|
||||||
@@ -710,13 +834,13 @@ class PlayerGames:
|
|||||||
return self.blackjack_prompt(data["prompt_player"])
|
return self.blackjack_prompt(data["prompt_player"])
|
||||||
if status == "insufficient":
|
if status == "insufficient":
|
||||||
return (
|
return (
|
||||||
f"{data['display_name']}:金币不足,余额 {data['balance']},"
|
f"{data['display_name']}:摩拉不足,余额 {data['balance']},"
|
||||||
f"本次需要 {data['bet']}"
|
f"本次需要 {data['bet']}"
|
||||||
)
|
)
|
||||||
if status == "bet_accepted":
|
if status == "bet_accepted":
|
||||||
return (
|
return (
|
||||||
f"{data['display_name']} 放下 {data['bet']};"
|
f"{data['display_name']} 放下 {data['bet']};"
|
||||||
f"金币 {data['balance']}\n"
|
f"摩拉 {data['balance']}\n"
|
||||||
f"{self.blackjack_prompt(data['prompt_player'])}"
|
f"{self.blackjack_prompt(data['prompt_player'])}"
|
||||||
)
|
)
|
||||||
if status == "dealt":
|
if status == "dealt":
|
||||||
@@ -814,7 +938,7 @@ class PlayerGames:
|
|||||||
f"{player['display_name']} #{player['player_game_index']}:"
|
f"{player['display_name']} #{player['player_game_index']}:"
|
||||||
f"{player['outcome']};牌 {self.blackjack_hand_text(player['hand'])};"
|
f"{player['outcome']};牌 {self.blackjack_hand_text(player['hand'])};"
|
||||||
f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};"
|
f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};"
|
||||||
f"金币 {player['balance']}"
|
f"摩拉 {player['balance']}"
|
||||||
)
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -864,11 +988,11 @@ class PlayerGames:
|
|||||||
return (
|
return (
|
||||||
"用法:/转账 对方 金额,例如 /转账 Bob 100;"
|
"用法:/转账 对方 金额,例如 /转账 Bob 100;"
|
||||||
"英文 /transfer Bob 100。金额范围 1~100000,"
|
"英文 /transfer Bob 100。金额范围 1~100000,"
|
||||||
"对方需先发送 /金币 初始化账户"
|
"对方需先发送 /摩拉 或 /金币 初始化账户"
|
||||||
)
|
)
|
||||||
target, amount = parsed
|
target, amount = parsed
|
||||||
if not MIN_TRANSFER_AMOUNT <= amount <= MAX_TRANSFER_AMOUNT:
|
if not MIN_TRANSFER_AMOUNT <= amount <= MAX_TRANSFER_AMOUNT:
|
||||||
return "转账金额必须为 1~100000 的整数金币"
|
return "转账金额必须为 1~100000 的整数摩拉"
|
||||||
|
|
||||||
def render(data: dict[str, Any]) -> str:
|
def render(data: dict[str, Any]) -> str:
|
||||||
source = data["source_display_name"]
|
source = data["source_display_name"]
|
||||||
@@ -877,18 +1001,18 @@ class PlayerGames:
|
|||||||
return f"{source}:不能给自己转账"
|
return f"{source}:不能给自己转账"
|
||||||
if data["status"] == "target_missing":
|
if data["status"] == "target_missing":
|
||||||
return (
|
return (
|
||||||
f"{source}:{target_name} 还没有金币账户,"
|
f"{source}:{target_name} 还没有摩拉账户,"
|
||||||
"请先让对方发送 /金币 初始化"
|
"请先让对方发送 /摩拉 或 /金币 初始化"
|
||||||
)
|
)
|
||||||
if data["status"] == "insufficient":
|
if data["status"] == "insufficient":
|
||||||
return (
|
return (
|
||||||
f"{source}:金币不足,余额 {data['source_balance']},"
|
f"{source}:摩拉不足,余额 {data['source_balance']},"
|
||||||
f"本次需要 {data['amount']}"
|
f"本次需要 {data['amount']}"
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
f"{source}:已向 {target_name} 转账 {data['amount']} 金币;"
|
f"{source}:已向 {target_name} 转账 {data['amount']} 摩拉;"
|
||||||
f"本人金币 {data['source_balance']};"
|
f"本人摩拉 {data['source_balance']};"
|
||||||
f"{target_name} 金币 {data['target_balance']}"
|
f"{target_name} 摩拉 {data['target_balance']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.ledger.transfer_player_coins(
|
return self.ledger.transfer_player_coins(
|
||||||
@@ -908,7 +1032,7 @@ class PlayerGames:
|
|||||||
return "用法:/贷款 [1~500];同一时间最多一笔未还贷款"
|
return "用法:/贷款 [1~500];同一时间最多一笔未还贷款"
|
||||||
amount = int(value)
|
amount = int(value)
|
||||||
if not MIN_LOAN_AMOUNT <= amount <= MAX_LOAN_AMOUNT:
|
if not MIN_LOAN_AMOUNT <= amount <= MAX_LOAN_AMOUNT:
|
||||||
return "贷款金额必须为 1~500 的整数金币"
|
return "贷款金额必须为 1~500 的整数摩拉"
|
||||||
else:
|
else:
|
||||||
amount = 0
|
amount = 0
|
||||||
if value not in {"", "全部", "all"}:
|
if value not in {"", "全部", "all"}:
|
||||||
@@ -916,7 +1040,7 @@ class PlayerGames:
|
|||||||
return "用法:/还款 [金额|全部]"
|
return "用法:/还款 [金额|全部]"
|
||||||
amount = int(value)
|
amount = int(value)
|
||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
return "还款金额必须为正整数金币"
|
return "还款金额必须为正整数摩拉"
|
||||||
|
|
||||||
group = str(row["group_fingerprint"])
|
group = str(row["group_fingerprint"])
|
||||||
sender = str(row["sender"])
|
sender = str(row["sender"])
|
||||||
@@ -929,7 +1053,7 @@ class PlayerGames:
|
|||||||
if kind == "loan":
|
if kind == "loan":
|
||||||
if loan:
|
if loan:
|
||||||
return (
|
return (
|
||||||
f"{player_label}:已有未还贷款 {loan['principal']} 金币;"
|
f"{player_label}:已有未还贷款 {loan['principal']} 摩拉;"
|
||||||
"请先 /还款 [金额|全部]"
|
"请先 /还款 [金额|全部]"
|
||||||
)
|
)
|
||||||
account["balance"] += amount
|
account["balance"] += amount
|
||||||
@@ -937,9 +1061,10 @@ class PlayerGames:
|
|||||||
"principal": amount,
|
"principal": amount,
|
||||||
"borrowed_at": time.time(),
|
"borrowed_at": time.time(),
|
||||||
}
|
}
|
||||||
|
append_coin_event(account, amount, "贷款到账")
|
||||||
return (
|
return (
|
||||||
f"{player_label}:贷款 {amount} 金币已到账;"
|
f"{player_label}:贷款 {amount} 摩拉已到账;"
|
||||||
f"当前金币 {account['balance']};未还贷款 {amount}"
|
f"当前摩拉 {account['balance']};未还贷款 {amount}"
|
||||||
)
|
)
|
||||||
if not loan:
|
if not loan:
|
||||||
return f"{player_label}:当前没有未还贷款"
|
return f"{player_label}:当前没有未还贷款"
|
||||||
@@ -949,13 +1074,14 @@ class PlayerGames:
|
|||||||
else:
|
else:
|
||||||
pay = min(amount, debt)
|
pay = min(amount, debt)
|
||||||
if pay <= 0:
|
if pay <= 0:
|
||||||
return f"{player_label}:金币不足,无法还款;未还贷款 {debt}"
|
return f"{player_label}:摩拉不足,无法还款;未还贷款 {debt}"
|
||||||
if pay > int(account["balance"]):
|
if pay > int(account["balance"]):
|
||||||
return (
|
return (
|
||||||
f"{player_label}:金币不足,余额 {account['balance']},"
|
f"{player_label}:摩拉不足,余额 {account['balance']},"
|
||||||
f"本次还款需要 {pay}"
|
f"本次还款需要 {pay}"
|
||||||
)
|
)
|
||||||
account["balance"] -= pay
|
account["balance"] -= pay
|
||||||
|
append_coin_event(account, -pay, "还款")
|
||||||
debt -= pay
|
debt -= pay
|
||||||
if debt:
|
if debt:
|
||||||
account["loan"] = {
|
account["loan"] = {
|
||||||
@@ -963,13 +1089,13 @@ class PlayerGames:
|
|||||||
"borrowed_at": float(loan.get("borrowed_at", time.time())),
|
"borrowed_at": float(loan.get("borrowed_at", time.time())),
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
f"{player_label}:已还款 {pay} 金币;"
|
f"{player_label}:已还款 {pay} 摩拉;"
|
||||||
f"剩余贷款 {debt};当前金币 {account['balance']}"
|
f"剩余贷款 {debt};当前摩拉 {account['balance']}"
|
||||||
)
|
)
|
||||||
account["loan"] = None
|
account["loan"] = None
|
||||||
return (
|
return (
|
||||||
f"{player_label}:已还清贷款 {pay} 金币;"
|
f"{player_label}:已还清贷款 {pay} 摩拉;"
|
||||||
f"当前金币 {account['balance']}"
|
f"当前摩拉 {account['balance']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.ledger.apply_player_action(
|
return self.ledger.apply_player_action(
|
||||||
@@ -1035,7 +1161,7 @@ class PlayerGames:
|
|||||||
if len(set(digits)) != 3:
|
if len(set(digits)) != 3:
|
||||||
raise ValueError("彩票的三个数字必须互不重复")
|
raise ValueError("彩票的三个数字必须互不重复")
|
||||||
if not MIN_LOTTERY_BET <= stake <= MAX_LOTTERY_BET:
|
if not MIN_LOTTERY_BET <= stake <= MAX_LOTTERY_BET:
|
||||||
raise ValueError("彩票下注必须为 1~500 的整数金币")
|
raise ValueError("彩票下注必须为 1~500 的整数摩拉")
|
||||||
return tuple(sorted(digits)), stake
|
return tuple(sorted(digits)), stake
|
||||||
|
|
||||||
def group_lottery(self, row: dict[str, Any], arguments: str) -> str:
|
def group_lottery(self, row: dict[str, Any], arguments: str) -> str:
|
||||||
@@ -1106,7 +1232,7 @@ class PlayerGames:
|
|||||||
f"命中 {entry['matches']} 个;下注 {entry['stake']},"
|
f"命中 {entry['matches']} 个;下注 {entry['stake']},"
|
||||||
f"返还 {entry['payout']},净变化 "
|
f"返还 {entry['payout']},净变化 "
|
||||||
f"{entry['payout'] - entry['stake']:+d};"
|
f"{entry['payout'] - entry['stake']:+d};"
|
||||||
f"金币 {entry['balance']}"
|
f"摩拉 {entry['balance']}"
|
||||||
)
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -1154,7 +1280,7 @@ class PlayerGames:
|
|||||||
return "当前没有进行中的彩票;先发送 /彩票 开始"
|
return "当前没有进行中的彩票;先发送 /彩票 开始"
|
||||||
if data["status"] == "insufficient":
|
if data["status"] == "insufficient":
|
||||||
return (
|
return (
|
||||||
f"{data['display_name']}:金币不足,余额 {data['balance']},"
|
f"{data['display_name']}:摩拉不足,余额 {data['balance']},"
|
||||||
f"报名需要 {stake}"
|
f"报名需要 {stake}"
|
||||||
)
|
)
|
||||||
action = "报名已更新" if data["status"] == "updated" else "报名成功"
|
action = "报名已更新" if data["status"] == "updated" else "报名成功"
|
||||||
@@ -1162,7 +1288,7 @@ class PlayerGames:
|
|||||||
f"{data['display_name']} #{data['player_game_index']}:"
|
f"{data['display_name']} #{data['player_game_index']}:"
|
||||||
f"彩票第 {data['round_number']} 把{action};号码 "
|
f"彩票第 {data['round_number']} 把{action};号码 "
|
||||||
f"{' '.join(map(str, data['numbers']))};下注 {data['stake']};"
|
f"{' '.join(map(str, data['numbers']))};下注 {data['stake']};"
|
||||||
f"金币 {data['balance']}"
|
f"摩拉 {data['balance']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.ledger.register_group_lottery(
|
return self.ledger.register_group_lottery(
|
||||||
@@ -1227,9 +1353,16 @@ class PlayerGames:
|
|||||||
payout=0,
|
payout=0,
|
||||||
summary="主动结束牌局,不退下注",
|
summary="主动结束牌局,不退下注",
|
||||||
)
|
)
|
||||||
|
append_game_coin_event(
|
||||||
|
account,
|
||||||
|
kind="blackjack",
|
||||||
|
outcome="主动认输",
|
||||||
|
stake=bet,
|
||||||
|
payout=0,
|
||||||
|
)
|
||||||
account["game"] = None
|
account["game"] = None
|
||||||
account["losses"] += 1
|
account["losses"] += 1
|
||||||
return f"本人的21点已认输结束,不退下注;金币 {account['balance']}"
|
return f"本人的21点已认输结束,不退下注;摩拉 {account['balance']}"
|
||||||
game["updated_at"] = time.time()
|
game["updated_at"] = time.time()
|
||||||
if action == "hit":
|
if action == "hit":
|
||||||
game["player"].append(game["deck"].pop())
|
game["player"].append(game["deck"].pop())
|
||||||
@@ -1269,7 +1402,14 @@ class PlayerGames:
|
|||||||
f"玩家{player}点,庄家{dealer}点"
|
f"玩家{player}点,庄家{dealer}点"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
append_game_coin_event(
|
||||||
|
account,
|
||||||
|
kind="blackjack",
|
||||||
|
outcome=outcome,
|
||||||
|
stake=int(bet),
|
||||||
|
payout=int(payout),
|
||||||
|
)
|
||||||
account["game"] = None
|
account["game"] = None
|
||||||
return (f"21点结算:{outcome};你的牌 {' '.join(map(card_label, game['player']))}({player}点),"
|
return (f"21点结算:{outcome};你的牌 {' '.join(map(card_label, game['player']))}({player}点),"
|
||||||
f"庄家 {' '.join(map(card_label, game['dealer']))}({dealer}点);"
|
f"庄家 {' '.join(map(card_label, game['dealer']))}({dealer}点);"
|
||||||
f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};金币 {account['balance']}")
|
f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};摩拉 {account['balance']}")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-sdk"
|
name = "jwebot-sdk"
|
||||||
version = "1.0.2b1+1016"
|
version = "1.0.3b1+1016"
|
||||||
description = "Typed, capability-limited extension API for JWeBot plugins"
|
description = "Typed, capability-limited extension API for JWeBot plugins"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
@@ -38,4 +38,4 @@ __all__ = [
|
|||||||
"SingleGroupContext",
|
"SingleGroupContext",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "1.0.2b1+1016"
|
__version__ = "1.0.3b1+1016"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tool.jwebot]
|
[tool.jwebot]
|
||||||
release = "beta 1.0.2.1016"
|
release = "beta 1.0.3.1016"
|
||||||
production_deployments_enabled_by_default = false
|
production_deployments_enabled_by_default = false
|
||||||
|
|
||||||
[tool.jwebot.workspace]
|
[tool.jwebot.workspace]
|
||||||
|
|||||||
@@ -677,6 +677,23 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
|||||||
self.application.deepseek.complete.assert_called_once()
|
self.application.deepseek.complete.assert_called_once()
|
||||||
self.application.deliver.assert_called_once()
|
self.application.deliver.assert_called_once()
|
||||||
|
|
||||||
|
def test_semantic_duplicate_with_new_event_id_is_ignored(self):
|
||||||
|
first = self.application.accept(
|
||||||
|
{**event("event-1", "@JWeBot /金币"), "source": "rapidocr-local"}
|
||||||
|
)
|
||||||
|
duplicate = self.application.accept(
|
||||||
|
{**event("event-2", "@JWeBot /金币"), "source": "rapidocr-local"}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(first["status"], "reply")
|
||||||
|
self.assertEqual(duplicate["status"], "ignored")
|
||||||
|
self.assertTrue(duplicate["duplicate"])
|
||||||
|
with self.application.ledger.lock:
|
||||||
|
count = self.application.ledger.connection.execute(
|
||||||
|
"SELECT COUNT(*) FROM events"
|
||||||
|
).fetchone()[0]
|
||||||
|
self.assertEqual(count, 1)
|
||||||
|
|
||||||
def test_failed_fast_ack_never_delivers_the_generated_reply(self):
|
def test_failed_fast_ack_never_delivers_the_generated_reply(self):
|
||||||
self.application.deepseek.complete = mock.Mock(return_value="最终回复喵")
|
self.application.deepseek.complete = mock.Mock(return_value="最终回复喵")
|
||||||
self.application.deliver = mock.Mock()
|
self.application.deliver = mock.Mock()
|
||||||
@@ -867,9 +884,9 @@ class LedgerAndApplicationTests(unittest.TestCase):
|
|||||||
self.assertEqual(version["route"], "local")
|
self.assertEqual(version["route"], "local")
|
||||||
self.assertIn("JWeBot 版本", version["text"])
|
self.assertIn("JWeBot 版本", version["text"])
|
||||||
self.assertIn("kernel", version["text"])
|
self.assertIn("kernel", version["text"])
|
||||||
self.assertIn("最新版本:beta 1.0.2.1016", version["text"])
|
self.assertIn("最新版本:beta 1.0.3.1016", version["text"])
|
||||||
self.assertIn("更新时间:2026-10-16", version["text"])
|
self.assertIn("更新时间:2026-10-16", version["text"])
|
||||||
self.assertIn("更新摘要:Cache OCR-negative", version["text"])
|
self.assertIn("更新摘要:Rename the virtual currency", version["text"])
|
||||||
self.assertEqual(debug["route"], "local")
|
self.assertEqual(debug["route"], "local")
|
||||||
self.assertIn("调试埋点", debug["text"])
|
self.assertIn("调试埋点", debug["text"])
|
||||||
self.assertEqual(balance["route"], "local")
|
self.assertEqual(balance["route"], "local")
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Release061Tests(unittest.TestCase):
|
|||||||
return json.loads(row[0])
|
return json.loads(row[0])
|
||||||
|
|
||||||
def test_aliases_are_corresponding_and_case_insensitive(self):
|
def test_aliases_are_corresponding_and_case_insensitive(self):
|
||||||
pairs = [("上下文", "Context"), ("金币", "wallet"), ("抽奖", "draw"),
|
pairs = [("上下文", "Context"), ("摩拉", "wallet"), ("抽奖", "draw"),
|
||||||
("买彩票", "lottery"), ("21点", "blackjack"),
|
("买彩票", "lottery"), ("21点", "blackjack"),
|
||||||
("人物列表", "people"), ("人物", "person"), ("游戏帮助", "games")]
|
("人物列表", "people"), ("人物", "person"), ("游戏帮助", "games")]
|
||||||
for chinese, english in pairs:
|
for chinese, english in pairs:
|
||||||
@@ -82,18 +82,18 @@ class Release061Tests(unittest.TestCase):
|
|||||||
self.assertEqual(ledger.search_repeat_history("group-a", "好"), [])
|
self.assertEqual(ledger.search_repeat_history("group-a", "好"), [])
|
||||||
|
|
||||||
def test_initial_funds_aliases_groups_and_replay(self):
|
def test_initial_funds_aliases_groups_and_replay(self):
|
||||||
self.assertIn("金币 1000", self.command("/wallet", event_id="wallet-1"))
|
self.assertIn("摩拉 1000", self.command("/wallet", event_id="wallet-1"))
|
||||||
with mock.patch.object(self.local.players.rng, "randrange", return_value=99):
|
with mock.patch.object(self.local.players.rng, "randrange", return_value=99):
|
||||||
first = self.command("/draw 10", event_id="draw-1")
|
first = self.command("/draw 10", event_id="draw-1")
|
||||||
replay = self.command("/draw 10", sender="a1", event_id="draw-1")
|
replay = self.command("/draw 10", sender="a1", event_id="draw-1")
|
||||||
self.assertEqual(first, replay)
|
self.assertEqual(first, replay)
|
||||||
self.assertEqual(self.state("a1")["balance"], 990)
|
self.assertEqual(self.state("a1")["balance"], 990)
|
||||||
self.assertIn("金币 1000", self.command("/wallet", sender="Bob"))
|
self.assertIn("摩拉 1000", self.command("/wallet", sender="Bob"))
|
||||||
self.assertIn("金币 1000", self.command("/wallet", group="group-b"))
|
self.assertIn("摩拉 1000", self.command("/wallet", group="group-b"))
|
||||||
self.engine.ledger.connection.close()
|
self.engine.ledger.connection.close()
|
||||||
self.engine = MODULE.EdgeApplication(config(self.root))
|
self.engine = MODULE.EdgeApplication(config(self.root))
|
||||||
self.local = self.engine.local
|
self.local = self.engine.local
|
||||||
self.assertIn("金币 990", self.command("/wallet", event_id="restart"))
|
self.assertIn("摩拉 990", self.command("/wallet", event_id="restart"))
|
||||||
|
|
||||||
def test_concurrent_replay_debits_once_and_rolls_back_failure(self):
|
def test_concurrent_replay_debits_once_and_rolls_back_failure(self):
|
||||||
replies = []
|
replies = []
|
||||||
@@ -123,7 +123,7 @@ class Release061Tests(unittest.TestCase):
|
|||||||
with mock.patch.object(self.local.players.rng, "randrange", return_value=99):
|
with mock.patch.object(self.local.players.rng, "randrange", return_value=99):
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
self.command("/draw 100")
|
self.command("/draw 100")
|
||||||
self.assertIn("金币不足", self.command("/draw 2"))
|
self.assertIn("摩拉不足", self.command("/draw 2"))
|
||||||
self.assertEqual(self.state()["balance"], 0)
|
self.assertEqual(self.state()["balance"], 0)
|
||||||
|
|
||||||
def test_blackjack_is_private_persistent_and_settles_exactly_once(self):
|
def test_blackjack_is_private_persistent_and_settles_exactly_once(self):
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ class Release062BlackjackTests(unittest.TestCase):
|
|||||||
self.assertIn("分两步", reply)
|
self.assertIn("分两步", reply)
|
||||||
self.assertIn("100 是本局下注", reply)
|
self.assertIn("100 是本局下注", reply)
|
||||||
self.assertIn("再单独发送", reply)
|
self.assertIn("再单独发送", reply)
|
||||||
wallet = self.command("/金币")
|
wallet = self.command("/摩拉")
|
||||||
self.assertIn("金币 1000", wallet)
|
self.assertIn("摩拉 1000", wallet)
|
||||||
self.assertIn("无进行中的牌局", wallet)
|
self.assertIn("无进行中的牌局", wallet)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+44
-23
@@ -215,9 +215,9 @@ class Release063GameTests(unittest.TestCase):
|
|||||||
settled = self.command("/21点 要牌", sender="Bob")
|
settled = self.command("/21点 要牌", sender="Bob")
|
||||||
self.assertIn("这一桌每个人的变化", settled)
|
self.assertIn("这一桌每个人的变化", settled)
|
||||||
self.assertIn("alice #1:你赢了", settled)
|
self.assertIn("alice #1:你赢了", settled)
|
||||||
self.assertIn("下注 200,返还 400,净变化 +200;金币 1200", settled)
|
self.assertIn("下注 200,返还 400,净变化 +200;摩拉 1200", settled)
|
||||||
self.assertIn("bob #1:你输了", settled)
|
self.assertIn("bob #1:你输了", settled)
|
||||||
self.assertIn("下注 100,返还 0,净变化 -100;金币 900", settled)
|
self.assertIn("下注 100,返还 0,净变化 -100;摩拉 900", settled)
|
||||||
self.assertEqual(self.state()["balance"], 1200)
|
self.assertEqual(self.state()["balance"], 1200)
|
||||||
self.assertEqual(self.state("Bob")["balance"], 900)
|
self.assertEqual(self.state("Bob")["balance"], 900)
|
||||||
self.assertIn("alice #1:21点 你赢了", self.command("/游戏记录"))
|
self.assertIn("alice #1:21点 你赢了", self.command("/游戏记录"))
|
||||||
@@ -225,6 +225,11 @@ class Release063GameTests(unittest.TestCase):
|
|||||||
"bob #1:21点 你输了",
|
"bob #1:21点 你输了",
|
||||||
self.command("/游戏记录", sender="Bob"),
|
self.command("/游戏记录", sender="Bob"),
|
||||||
)
|
)
|
||||||
|
self.assertIn("+200:21点 获胜", self.command("/金币"))
|
||||||
|
self.assertIn(
|
||||||
|
"-100:21点 失败",
|
||||||
|
self.command("/金币", sender="Bob"),
|
||||||
|
)
|
||||||
|
|
||||||
def test_prize_draw_has_indexed_history(self):
|
def test_prize_draw_has_indexed_history(self):
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
@@ -242,12 +247,12 @@ class Release063GameTests(unittest.TestCase):
|
|||||||
self.assertIn("alice #1:抽奖 未中奖", history)
|
self.assertIn("alice #1:抽奖 未中奖", history)
|
||||||
|
|
||||||
def test_transfer_requires_initialized_target_and_replays_once(self):
|
def test_transfer_requires_initialized_target_and_replays_once(self):
|
||||||
self.assertIn("金币 1000", self.command("/金币"))
|
self.assertIn("摩拉 1000", self.command("/摩拉"))
|
||||||
missing = self.command("/转账 Bob 100", event_id="missing-target")
|
missing = self.command("/转账 Bob 100", event_id="missing-target")
|
||||||
self.assertIn("bob 还没有金币账户", missing)
|
self.assertIn("bob 还没有摩拉账户", missing)
|
||||||
self.assertEqual(self.state()["balance"], 1000)
|
self.assertEqual(self.state()["balance"], 1000)
|
||||||
|
|
||||||
self.assertIn("金币 1000", self.command("/金币", sender="Bob"))
|
self.assertIn("摩拉 1000", self.command("/金币", sender="Bob"))
|
||||||
first = self.command("/转账 Bob 250", event_id="transfer-1")
|
first = self.command("/转账 Bob 250", event_id="transfer-1")
|
||||||
replay = self.command(
|
replay = self.command(
|
||||||
"/转账 Bob 250",
|
"/转账 Bob 250",
|
||||||
@@ -256,40 +261,56 @@ class Release063GameTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(replay, first)
|
self.assertEqual(replay, first)
|
||||||
self.assertIn("alice:已向 bob 转账 250 金币", first)
|
self.assertIn("alice:已向 bob 转账 250 摩拉", first)
|
||||||
self.assertEqual(self.state()["balance"], 750)
|
self.assertEqual(self.state()["balance"], 750)
|
||||||
self.assertEqual(self.state("Bob")["balance"], 1250)
|
self.assertEqual(self.state("Bob")["balance"], 1250)
|
||||||
|
alice_wallet = self.command("/金币", event_id="wallet-after-transfer-a")
|
||||||
|
bob_wallet = self.command(
|
||||||
|
"/金币",
|
||||||
|
sender="Bob",
|
||||||
|
event_id="wallet-after-transfer-b",
|
||||||
|
)
|
||||||
|
self.assertIn("alice:当前摩拉 750;无贷款;", alice_wallet)
|
||||||
|
self.assertIn("最近10次账户变动:", alice_wallet)
|
||||||
|
self.assertIn("-250:转账给 bob", alice_wallet)
|
||||||
|
self.assertIn("+1000:首次初始化", alice_wallet)
|
||||||
|
self.assertIn("bob:当前摩拉 1250;无贷款;", bob_wallet)
|
||||||
|
self.assertIn("+250:转账 来自 alice", bob_wallet)
|
||||||
|
|
||||||
def test_transfer_rejects_invalid_self_and_insufficient_cases(self):
|
def test_transfer_rejects_invalid_self_and_insufficient_cases(self):
|
||||||
self.assertIn("金币 1000", self.command("/金币"))
|
self.assertIn("摩拉 1000", self.command("/金币"))
|
||||||
self.assertIn("转账金额必须为 1~100000", self.command("/转账 Bob 0"))
|
self.assertIn("转账金额必须为 1~100000", self.command("/转账 Bob 0"))
|
||||||
self.assertIn("不能给自己转账", self.command("/转账 a1 100"))
|
self.assertIn("不能给自己转账", self.command("/转账 a1 100"))
|
||||||
self.assertIn("金币 1000", self.command("/金币", sender="Bob"))
|
self.assertIn("摩拉 1000", self.command("/金币", sender="Bob"))
|
||||||
self.assertIn("金币不足", self.command("/transfer 1001 Bob"))
|
self.assertIn("摩拉不足", self.command("/transfer 1001 Bob"))
|
||||||
self.assertEqual(self.state()["balance"], 1000)
|
self.assertEqual(self.state()["balance"], 1000)
|
||||||
self.assertEqual(self.state("Bob")["balance"], 1000)
|
self.assertEqual(self.state("Bob")["balance"], 1000)
|
||||||
|
|
||||||
def test_loan_repay_and_replay_once(self):
|
def test_loan_repay_and_replay_once(self):
|
||||||
self.assertIn("金币 1000", self.command("/金币"))
|
self.assertIn("摩拉 1000", self.command("/金币"))
|
||||||
first = self.command("/贷款 300", event_id="loan-1")
|
first = self.command("/贷款 300", event_id="loan-1")
|
||||||
replay = self.command("/贷款 300", event_id="loan-1")
|
replay = self.command("/贷款 300", event_id="loan-1")
|
||||||
|
|
||||||
self.assertEqual(replay, first)
|
self.assertEqual(replay, first)
|
||||||
self.assertIn("贷款 300 金币已到账", first)
|
self.assertIn("贷款 300 摩拉已到账", first)
|
||||||
self.assertEqual(self.state()["balance"], 1300)
|
self.assertEqual(self.state()["balance"], 1300)
|
||||||
self.assertEqual(self.state()["loan"]["principal"], 300)
|
self.assertEqual(self.state()["loan"]["principal"], 300)
|
||||||
self.assertIn("未还贷款 300", self.command("/金币"))
|
self.assertIn("未还贷款 300", self.command("/金币"))
|
||||||
self.assertIn("已有未还贷款 300", self.command("/贷款 100"))
|
self.assertIn("已有未还贷款 300", self.command("/贷款 100"))
|
||||||
|
|
||||||
partial = self.command("/还款 120")
|
partial = self.command("/还款 120")
|
||||||
self.assertIn("已还款 120 金币", partial)
|
self.assertIn("已还款 120 摩拉", partial)
|
||||||
self.assertEqual(self.state()["balance"], 1180)
|
self.assertEqual(self.state()["balance"], 1180)
|
||||||
self.assertEqual(self.state()["loan"]["principal"], 180)
|
self.assertEqual(self.state()["loan"]["principal"], 180)
|
||||||
|
|
||||||
done = self.command("/还款 全部")
|
done = self.command("/还款 全部")
|
||||||
self.assertIn("已还清贷款 180 金币", done)
|
self.assertIn("已还清贷款 180 摩拉", done)
|
||||||
self.assertEqual(self.state()["balance"], 1000)
|
self.assertEqual(self.state()["balance"], 1000)
|
||||||
self.assertIsNone(self.state()["loan"])
|
self.assertIsNone(self.state()["loan"])
|
||||||
|
wallet = self.command("/金币", event_id="wallet-after-repay")
|
||||||
|
self.assertIn("+300:贷款到账", wallet)
|
||||||
|
self.assertIn("-120:还款", wallet)
|
||||||
|
self.assertIn("-180:还款", wallet)
|
||||||
|
|
||||||
def test_loan_rejects_invalid_amounts_and_empty_repay(self):
|
def test_loan_rejects_invalid_amounts_and_empty_repay(self):
|
||||||
self.assertIn("贷款金额必须为 1~500", self.command("/贷款 0"))
|
self.assertIn("贷款金额必须为 1~500", self.command("/贷款 0"))
|
||||||
@@ -297,20 +318,20 @@ class Release063GameTests(unittest.TestCase):
|
|||||||
self.assertIn("当前没有未还贷款", self.command("/还款 全部"))
|
self.assertIn("当前没有未还贷款", self.command("/还款 全部"))
|
||||||
|
|
||||||
def test_coin_ranking_lists_accounts_by_balance(self):
|
def test_coin_ranking_lists_accounts_by_balance(self):
|
||||||
self.assertIn("金币 1000", self.command("/金币"))
|
self.assertIn("摩拉 1000", self.command("/金币"))
|
||||||
self.assertIn("金币 1000", self.command("/金币", sender="Bob"))
|
self.assertIn("摩拉 1000", self.command("/金币", sender="Bob"))
|
||||||
self.assertIn("金币 1000", self.command("/金币", sender="Carol"))
|
self.assertIn("摩拉 1000", self.command("/金币", sender="Carol"))
|
||||||
self.command("/转账 Bob 250")
|
self.command("/转账 Bob 250")
|
||||||
self.command("/贷款 100", sender="Carol")
|
self.command("/贷款 100", sender="Carol")
|
||||||
|
|
||||||
ranking = self.command("/大富翁榜")
|
ranking = self.command("/大富翁榜")
|
||||||
|
|
||||||
self.assertIn("大富翁榜:", ranking)
|
self.assertIn("大富翁榜:", ranking)
|
||||||
self.assertIn("1. bob 1250 金币", ranking)
|
self.assertIn("1. bob 1250 摩拉", ranking)
|
||||||
self.assertIn("2. carol 1100 金币(未还贷款 100)", ranking)
|
self.assertIn("2. carol 1100 摩拉(未还贷款 100)", ranking)
|
||||||
self.assertIn("3. alice 750 金币", ranking)
|
self.assertIn("3. alice 750 摩拉", ranking)
|
||||||
self.assertNotIn("Dave", ranking)
|
self.assertNotIn("Dave", ranking)
|
||||||
self.assertIn("金币排行人数必须为 1~20", self.command("/金币排行 0"))
|
self.assertIn("摩拉排行人数必须为 1~20", self.command("/摩拉排行 0"))
|
||||||
|
|
||||||
def test_taffy_coin_stats_sums_all_game_history(self):
|
def test_taffy_coin_stats_sums_all_game_history(self):
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
@@ -326,14 +347,14 @@ class Release063GameTests(unittest.TestCase):
|
|||||||
):
|
):
|
||||||
self.command("/抽奖10", sender="Bob")
|
self.command("/抽奖10", sender="Bob")
|
||||||
|
|
||||||
stats = self.command("/Taffy金币")
|
stats = self.command("/Taffy摩拉")
|
||||||
|
|
||||||
self.assertIn("玩家总下注 20,总返还 50", stats)
|
self.assertIn("玩家总下注 20,总返还 50", stats)
|
||||||
self.assertIn("Taffy累计倒贴 30 金币", stats)
|
self.assertIn("Taffy累计倒贴 30 摩拉", stats)
|
||||||
self.assertIn("最大上贡者排行(含负数):", stats)
|
self.assertIn("最大上贡者排行(含负数):", stats)
|
||||||
self.assertIn("alice:上贡 10", stats)
|
self.assertIn("alice:上贡 10", stats)
|
||||||
self.assertIn("bob:从Taffy赢走 40", stats)
|
self.assertIn("bob:从Taffy赢走 40", stats)
|
||||||
self.assertIn("Taffy金币人数必须为 1~20", self.command("/Taffy金币 0"))
|
self.assertIn("Taffy摩拉人数必须为 1~20", self.command("/Taffy摩拉 0"))
|
||||||
|
|
||||||
def test_open_round_and_history_survive_restart(self):
|
def test_open_round_and_history_survive_restart(self):
|
||||||
root = Path(self.temp.name)
|
root = Path(self.temp.name)
|
||||||
|
|||||||
@@ -423,6 +423,31 @@ class LayoutScalingTests(unittest.TestCase):
|
|||||||
fast_ack_sender.assert_called_once()
|
fast_ack_sender.assert_called_once()
|
||||||
legacy.assert_not_called()
|
legacy.assert_not_called()
|
||||||
|
|
||||||
|
def test_ignored_duplicate_does_not_fall_back(self):
|
||||||
|
legacy = mock.Mock(return_value="legacy")
|
||||||
|
response = {
|
||||||
|
"status": "ignored",
|
||||||
|
"route": "local",
|
||||||
|
"execution_path": "local_command",
|
||||||
|
"duplicate": True,
|
||||||
|
}
|
||||||
|
with mock.patch.object(
|
||||||
|
MODULE.jwebot_edge,
|
||||||
|
"request_edge",
|
||||||
|
return_value=response,
|
||||||
|
):
|
||||||
|
reply = MODULE.edge_router_reply(
|
||||||
|
{
|
||||||
|
"event_id": "event-2",
|
||||||
|
"group_fingerprint": "group-a",
|
||||||
|
"text": "@JWeBot /金币",
|
||||||
|
},
|
||||||
|
legacy_router=legacy,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(reply, "NO_REPLY")
|
||||||
|
legacy.assert_not_called()
|
||||||
|
|
||||||
def test_hash_command_uses_legacy_router_when_edge_is_unavailable(self):
|
def test_hash_command_uses_legacy_router_when_edge_is_unavailable(self):
|
||||||
legacy = mock.Mock(return_value="legacy")
|
legacy = mock.Mock(return_value="legacy")
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
|
|||||||
@@ -761,6 +761,18 @@ def edge_router_reply(
|
|||||||
outcome="reply",
|
outcome="reply",
|
||||||
)
|
)
|
||||||
return reply
|
return reply
|
||||||
|
if response.get("status") == "ignored":
|
||||||
|
TELEMETRY.elapsed(
|
||||||
|
"recorder.edge.request",
|
||||||
|
started,
|
||||||
|
event_id=event_id,
|
||||||
|
group_id=group_id,
|
||||||
|
route=str(response.get("route", "")),
|
||||||
|
execution_path=str(response.get("execution_path", "")),
|
||||||
|
outcome="ignored",
|
||||||
|
duplicate=bool(response.get("duplicate")),
|
||||||
|
)
|
||||||
|
return "NO_REPLY"
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"edge router fallback event_hash=%s status=%s",
|
"edge router fallback event_hash=%s status=%s",
|
||||||
hashlib.sha256(
|
hashlib.sha256(
|
||||||
|
|||||||
Reference in New Issue
Block a user