diff --git a/CHANGELOG.md b/CHANGELOG.md index aea9859..1234a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 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 - Cache OCR-negative visible bubbles and reuse stable visual signatures as the diff --git a/README.md b/README.md index 76fb63a..6d8ce8f 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC 观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件, 并通过带视觉确认的 GUI 发送链路回复。 -当前版本:`beta 1.0.2.1016`,更新日期:2026-10-16。 +当前版本:`beta 1.0.3.1016`,更新日期:2026-10-16。 1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、 -Steam 状态与时长查询、虚拟金币游戏、多人彩票、21 点、CS Major +Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major 自走棋、生产部署与回滚门禁。CS Major 自走棋的选手、教练、叙事和规则 -已经外置为包内 JSON,虚拟金币支持转账、贷款、还款和群内排行榜。 +已经外置为包内 JSON,虚拟摩拉支持转账、贷款、还款和群内排行榜。 完整命令见[命令目录](docs/command-catalog.md)。历史版本记录见 [CHANGELOG](CHANGELOG.md)。 @@ -82,8 +82,8 @@ PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh /功能列表 /上下文 [100|200|300](/context) /人物列表 [页码](/people);/人物 [名称或别名](/person) -/金币(/wallet);/抽奖 [金额](/draw) -/大富翁榜(/金币排行) +/摩拉(兼容 /金币、/wallet);/抽奖 [金额](/draw) +/大富翁榜(/摩拉排行,兼容 /金币排行) /转账 对方 金额(/transfer name amount) /贷款 [金额](/loan amount);/还款 [金额|全部](/repay) /彩票 开始|报名|当前状态|开奖|取消(/lottery) @@ -263,7 +263,7 @@ visually confirmed GUI bridge. Current version: ```text -beta 1.0.2.1016 +beta 1.0.3.1016 ``` Updated: 2026-10-16. diff --git a/VERSION b/VERSION index c650b31..3643e99 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -beta 1.0.2.1016 +beta 1.0.3.1016 diff --git a/docs/command-catalog.md b/docs/command-catalog.md index 95b11f7..548d8c4 100644 --- a/docs/command-catalog.md +++ b/docs/command-catalog.md @@ -41,8 +41,8 @@ descriptor inventory; plugin code delegates to reviewed service profiles. | Command | Alias | Purpose | |---|---|---| -| `/金币` | `/wallet` | Initialize or show the current person's virtual coin account. | -| `/大富翁榜 [人数]` | `/金币排行`, `/金币排行榜`, `/richlist` | Show the group-scoped coin leaderboard. | +| `/摩拉` | `/金币`, `/wallet` | Initialize or show the current person's Mora account. | +| `/大富翁榜 [人数]` | `/摩拉排行`, `/摩拉排行榜`, `/金币排行`, `/金币排行榜`, `/richlist` | Show the group-scoped Mora leaderboard. | | `/转账 对方 金额` | `/transfer name amount` | Transfer virtual coins to an initialized group account. | | `/贷款 [金额]` | `/loan amount`, `/借款` | Borrow up to 500 virtual coins. | | `/还款 [金额|全部]` | `/repay [amount|all]` | Repay an active virtual coin loan. | diff --git a/docs/cs-major-autochess-design.md b/docs/cs-major-autochess-design.md index c2f2439..2a1aad8 100644 --- a/docs/cs-major-autochess-design.md +++ b/docs/cs-major-autochess-design.md @@ -48,7 +48,7 @@ 每位玩家开局拥有: - 生命值:`100` -- 金币:`10` +- 摩拉:`10` - 教练:赛前第一轮选秀决定 - 初始选手:赛前第二、三轮选秀决定,系统随后补齐阵容 - 阵容位:固定 `5 个选手位 + 1 个教练位`,比赛时必须全部上场 @@ -206,7 +206,7 @@ ### 7.2 教练获取 -教练由第一轮公共选秀决定,本届赛事内不能用金币升级教练。不同教练 +教练由第一轮公共选秀决定,本届赛事内不能用摩拉升级教练。不同教练 自带等级、属性倾向和全队加成。 高费选手的商店概率随赛事进程推进,不再绑定教练等级。 @@ -270,7 +270,7 @@ - 第一轮教练选秀展示固定 10 名教练;第二、三轮选手选秀展示 6 名选手。 - 训练赛和正赛运营阶段默认展示一组 6 人商店。 -- 玩家可支付金币执行刷新。 +- 玩家可支付摩拉执行刷新。 - 玩家可锁定本轮 6 人,使下一轮仍保留同一组候选。 ### 8.4 顺位 @@ -294,7 +294,7 @@ ## 10. 训练赛与赛事挑战 训练赛不再是阈值型 PvE。它是正式小组赛前的多轮真实对阵,用来给玩家 -检验阵容、获取金币并自然插入 D 牌窗口。媒体日和赞助挑战仍作为赛事内 +检验阵容、获取摩拉并自然插入 D 牌窗口。媒体日和赞助挑战仍作为赛事内 插曲出现,但只提供资源变动,不淘汰玩家。 ### 10.1 活动类型 @@ -308,11 +308,11 @@ 训练赛按参赛队伍生成 round-robin 轮次。四队测试局会有三轮训练赛, 每队在训练阶段遇到另外三支队伍;奇数队伍时轮空队会与本地陪练队热身。 -训练赛只发放金币,不记录小组胜负,也不扣生命。 +训练赛只发放摩拉,不记录小组胜负,也不扣生命。 每轮训练赛结束后进入运营窗口: -- `/刷新` 即 D 牌,消耗金币换一组 6 人商店; +- `/刷新` 即 D 牌,消耗摩拉换一组 6 人商店; - `/选1~/选6` 买入商店选手,重复卡自动累积星级; - `/锁定` 保留当前商店; - `/上阵` 调整 5 人首发; @@ -321,10 +321,10 @@ ### 10.3 训练赛结算逻辑 训练赛使用与正式比赛相同的单图 MR15 逐回合模拟,只是赛制为 BO1。 -胜方获得更多金币,负方也有保底金币,保证测试局能继续运营。 +胜方获得更多摩拉,负方也有保底摩拉,保证测试局能继续运营。 媒体日和赞助挑战根据阵容完整度、教练、战术和本地确定性随机因子发放 -金币,但播报不展示“目标/发挥”这类计算日志,只写剧情和奖励。 +摩拉,但播报不展示“目标/发挥”这类计算日志,只写剧情和奖励。 ## 11. PvP 赛制 @@ -618,7 +618,7 @@ Jacky 选择了 zonic! - AI 测试战队加入 - 赛制与种子公布 - 三轮选秀候选池、选择结果和选秀完成 -- 每轮训练赛对阵、比分、金币和运营提示 +- 每轮训练赛对阵、比分、摩拉和运营提示 - 媒体日或赞助挑战 - 每场 BO3/BO5 对阵与结果 - 每张加时地图 diff --git a/docs/group-lottery-0.6.3.md b/docs/group-lottery-0.6.3.md index 1640453..0c80b68 100644 --- a/docs/group-lottery-0.6.3.md +++ b/docs/group-lottery-0.6.3.md @@ -4,7 +4,7 @@ 1. 任意群友发送 `/彩票 开始`。发起人是本轮主持人。 2. 每个人发送 `/彩票 报名 3 4 7 100`,表示选择 3、4、7,下注 100 - 金币。也可简写为 `/彩票347100`;省略金额时默认下注 10。 + 摩拉。也可简写为 `/彩票347100`;省略金额时默认下注 10。 3. 发送 `/彩票 当前状态` 查看本轮主持人、报名人数、每个人的号码和下注。 4. 主持人发送 `/彩票 开奖`。机器人从 0~9 中抽取三个互不重复的数字, 统一排序后结算。 @@ -45,7 +45,7 @@ | 2 个 | 120% | | 3 个 | 300% | -金币只使用整数;百分比计算的小数部分向下取整。例如下注 100,命中两个 +摩拉只使用整数;百分比计算的小数部分向下取整。例如下注 100,命中两个 返还 120,净变化为 `+20`。 ## 个人局号与记录 diff --git a/docs/local-plugins-0.6.1.md b/docs/local-plugins-0.6.1.md index 57229bf..171b7f5 100644 --- a/docs/local-plugins-0.6.1.md +++ b/docs/local-plugins-0.6.1.md @@ -7,8 +7,8 @@ OpenClaw;中文、英文命令对应同一实现,英文不区分大小写, | 插件 | 中文指令 | 英文指令 | |---|---|---| | context-summary | `/上下文 [100|200|300]` | `/context [100|200|300]` | -| single-player-games | `/金币` | `/wallet` | -| coin-ranking | `/大富翁榜`、`/金币排行` | `/richlist` | +| single-player-games | `/摩拉`(兼容 `/金币`) | `/wallet` | +| coin-ranking | `/大富翁榜`、`/摩拉排行`(兼容 `/金币排行`) | `/richlist` | | coin-transfer | `/转账 对方 金额` | `/transfer name amount` | | coin-loan | `/贷款 [金额]`、`/还款 [金额\|全部]` | `/loan amount`、`/repay [amount\|all]` | | single-player-games | `/抽奖 [金额]` | `/draw [金额]` | @@ -23,7 +23,7 @@ OpenClaw;中文、英文命令对应同一实现,英文不区分大小写, 命令所有权来自 `descriptors.py`,由同步工具生成三套受审核 SDK adapter。 每个 adapter 只提交命名 service-profile 请求,由 Kernel 执行数据库操作。 注册在启动期完成,运行期冻结。`/余额` 保持查询提供商余额; -游戏虚拟余额使用 `/金币`。 +游戏虚拟余额使用 `/摩拉`,`/金币` 保留为兼容别名。 ## 上下文总结 @@ -43,7 +43,7 @@ OpenClaw;中文、英文命令对应同一实现,英文不区分大小写, 当前 OCR 输入只有显示名,没有微信原生账号 ID。Kernel 将昵称或别名 解析到私有配置中的 canonical_name,再以“当前群+规范人物名”计算 -person_key。相同人物的已配置别名共用金币和牌局,其他群使用独立账户。 +person_key。相同人物的已配置别名共用摩拉和牌局,其他群使用独立账户。 没有配置的显示名使用独立临时身份;未识别到发送人的消息不能操作账户。 `/人物` 默认查看本人;指定参数时只查询已配置人物。输出包括展示名、 @@ -53,27 +53,27 @@ person_key。相同人物的已配置别名共用金币和牌局,其他群使 昵称识别不是账号身份认证。修改 canonical_name 会改变当前人物作用域; 应优先追加别名而不是重命名。除 `/转账` 外,用户不能通过游戏命令 -指定他人的账户;转账只支持同群虚拟金币账户,收款人需先发送 `/金币` -初始化。`/贷款` 和 `/还款` 只作用于本人账户。虚拟金币不能充值、 +指定他人的账户;转账只支持同群虚拟摩拉账户,收款人需先发送 `/摩拉` +或 `/金币` 初始化。`/贷款` 和 `/还款` 只作用于本人账户。虚拟摩拉不能充值、 赠币或兑换真实资产。 -## 虚拟金币与游戏管理 +## 虚拟摩拉与游戏管理 -每个群内人物首次使用账户时一次性获得 1000 虚拟金币。再次查询、 +每个群内人物首次使用账户时一次性获得 1000 虚拟摩拉。再次查询、 重启服务或切换已配置别名都不会重复领取。抽奖下注为 2~100,彩票和 21 点下注为 1~500;余额不足或参数非法不扣款。 -`/转账 对方 金额` 会把本人的虚拟金币转给同群已初始化账户,英文为 +`/转账 对方 金额` 会把本人的虚拟摩拉转给同群已初始化账户,英文为 `/transfer name amount`。金额范围 1~100000;不允许给自己转账。 转账、响应记录和重试去重写在同一个 ledger 事务内,同一 event_id 不会重复扣款。 -`/贷款 [金额]` 会给本人账户借入 1~500 金币;同一账户同一时间最多 -一笔未还贷款。`/还款 [金额|全部]` 从当前金币余额中扣除并减少未还 +`/贷款 [金额]` 会给本人账户借入 1~500 摩拉;同一账户同一时间最多 +一笔未还贷款。`/还款 [金额|全部]` 从当前摩拉余额中扣除并减少未还 贷款,借款、还款、响应记录和重试去重同样写在一个 ledger 事务内。 -`/大富翁榜` 或 `/金币排行 [人数]` 只读取当前群已有金币账户,不会创建 -调用者账户。榜单按当前金币余额降序排列,人数范围 1~20;账户存在 +`/大富翁榜` 或 `/摩拉排行 [人数]`(兼容 `/金币排行`)只读取当前群已有摩拉账户,不会创建 +调用者账户。榜单按当前摩拉余额降序排列,人数范围 1~20;账户存在 未还贷款时会在名字后标注贷款本金。 抽奖默认下注 10:5% 概率返还五倍下注,20% 返还两倍,其余无返还。 @@ -81,7 +81,7 @@ alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数 下注范围 1~500,命中 1/2/3 个分别返还 50%/120%/300%。完整流程见 [多人彩票与游戏记录](group-lottery-0.6.3.md)。 -21 点默认下注 50、范围 1~500。`/21点 开始 100` 表示拿 100 金币作为本局下注进场, +21 点默认下注 50、范围 1~500。`/21点 开始 100` 表示拿 100 摩拉作为本局下注进场, 等价的 OCR 紧凑形式是 `/21点开始100`。开局会扣除下注并发两张牌, 然后根据牌面单独发送 `/21点 要牌` 或 `/21点 停牌`;开局与操作写在 同一条消息时不会扣款,而会返回分步提示。牌局使用洗牌后的单副 52 张牌, @@ -95,7 +95,7 @@ alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数 其他玩家在下注前用 `/21点 加入` 进入当前牌桌;主持人发送 `/21点 多人 开始` 后,Taffy 会按座位顺序提示每个人 `/21点 放200` 放筹码,范围 1~10000。 所有玩家下注后统一发牌,轮到本人时用 `/21点 要牌` 或 `/21点 停牌` 操作; -结算按座位顺序列出每个人的下注、返还、净变化和当前金币。 +结算按座位顺序列出每个人的下注、返还、净变化和当前摩拉。 自然 21 点立即结算,返还 2.5 倍下注向下取整;普通胜利返还两倍, 平局返还下注,失败不返还。每人最多一局;牌局进行中不接受另一笔 抽奖或 21 点开局,多人彩票报名仍可独立进行。 @@ -106,7 +106,7 @@ alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数 Kernel 用一个 `BEGIN IMMEDIATE` 事务同时保存: -1. `player_accounts` 中的金币、牌局和胜负统计; +1. `player_accounts` 中的摩拉、牌局和胜负统计; 2. `player_actions` 中的事件、命令及响应。 同一群中的同一 event_id 只结算一次,重试返回原响应。相同事件若变更 diff --git a/extensions/jwebot/coin-ranking/README.md b/extensions/jwebot/coin-ranking/README.md index 9b9aaa0..75d5695 100644 --- a/extensions/jwebot/coin-ranking/README.md +++ b/extensions/jwebot/coin-ranking/README.md @@ -10,6 +10,8 @@ Help: Registered commands: +- `/摩拉排行` +- `/摩拉排行榜` - `/金币排行` - `/金币排行榜` - `/大富翁榜` diff --git a/extensions/jwebot/coin-ranking/plugin.toml b/extensions/jwebot/coin-ranking/plugin.toml index b1228a4..3b0b148 100644 --- a/extensions/jwebot/coin-ranking/plugin.toml +++ b/extensions/jwebot/coin-ranking/plugin.toml @@ -8,7 +8,7 @@ entrypoint = "coin_ranking:plugin" type = "command" [capabilities] -commands = ["/金币排行", "/金币排行榜", "/大富翁榜", "/richlist"] +commands = ["/摩拉排行", "/摩拉排行榜", "/金币排行", "/金币排行榜", "/大富翁榜", "/richlist"] storage = false network_profiles = [] service_profiles = ["coin-ranking"] diff --git a/extensions/jwebot/coin-ranking/src/coin_ranking.py b/extensions/jwebot/coin-ranking/src/coin_ranking.py index 938f8aa..9374bab 100644 --- a/extensions/jwebot/coin-ranking/src/coin_ranking.py +++ b/extensions/jwebot/coin-ranking/src/coin_ranking.py @@ -7,7 +7,7 @@ PROFILE = 'coin-ranking' AUTHOR = 'jwebot' DESCRIPTION = '富榜' HELP_ENTRIES = ('/大富翁榜',) -COMMANDS = ('/金币排行', '/金币排行榜', '/大富翁榜', '/richlist') +COMMANDS = ('/摩拉排行', '/摩拉排行榜', '/金币排行', '/金币排行榜', '/大富翁榜', '/richlist') def _handler(context: SingleGroupContext, command: str): diff --git a/extensions/jwebot/coin-ranking/tests/test_plugin.py b/extensions/jwebot/coin-ranking/tests/test_plugin.py index 4eff247..ed18d67 100644 --- a/extensions/jwebot/coin-ranking/tests/test_plugin.py +++ b/extensions/jwebot/coin-ranking/tests/test_plugin.py @@ -21,8 +21,8 @@ class PluginTests(unittest.TestCase): self.assertEqual(AUTHOR, 'jwebot') self.assertEqual(DESCRIPTION, '富榜') self.assertEqual(HELP_ENTRIES, ('/大富翁榜',)) - self.assertIn('/金币排行', registered) - self.assertEqual(len(registered), 4) + self.assertIn('/摩拉排行', registered) + self.assertEqual(len(registered), 6) if __name__ == "__main__": diff --git a/extensions/jwebot/coin-taffy/README.md b/extensions/jwebot/coin-taffy/README.md index ac7b071..520e3bb 100644 --- a/extensions/jwebot/coin-taffy/README.md +++ b/extensions/jwebot/coin-taffy/README.md @@ -2,14 +2,15 @@ Author: `jwebot` -Taffy金币 +Taffy摩拉 Help: -- `/Taffy金币` +- `/Taffy摩拉(兼容 /Taffy金币)` Registered commands: +- `/Taffy摩拉` - `/Taffy金币` - `/taffycoins` diff --git a/extensions/jwebot/coin-taffy/plugin.toml b/extensions/jwebot/coin-taffy/plugin.toml index 0b5f668..f6dd50b 100644 --- a/extensions/jwebot/coin-taffy/plugin.toml +++ b/extensions/jwebot/coin-taffy/plugin.toml @@ -1,14 +1,14 @@ id = "coin-taffy" author = "jwebot" -description = "Taffy金币" -help = ["/Taffy金币"] +description = "Taffy摩拉" +help = ["/Taffy摩拉(兼容 /Taffy金币)"] version = "1.0.0" api_version = "1" entrypoint = "coin_taffy:plugin" type = "command" [capabilities] -commands = ["/Taffy金币", "/taffycoins"] +commands = ["/Taffy摩拉", "/Taffy金币", "/taffycoins"] storage = false network_profiles = [] service_profiles = ["coin-taffy"] diff --git a/extensions/jwebot/coin-taffy/src/coin_taffy.py b/extensions/jwebot/coin-taffy/src/coin_taffy.py index 08974dd..0b1836a 100644 --- a/extensions/jwebot/coin-taffy/src/coin_taffy.py +++ b/extensions/jwebot/coin-taffy/src/coin_taffy.py @@ -5,9 +5,9 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'coin-taffy' AUTHOR = 'jwebot' -DESCRIPTION = 'Taffy金币' -HELP_ENTRIES = ('/Taffy金币',) -COMMANDS = ('/Taffy金币', '/taffycoins') +DESCRIPTION = 'Taffy摩拉' +HELP_ENTRIES = ('/Taffy摩拉(兼容 /Taffy金币)',) +COMMANDS = ('/Taffy摩拉', '/Taffy金币', '/taffycoins') def _handler(context: SingleGroupContext, command: str): diff --git a/extensions/jwebot/coin-taffy/tests/test_plugin.py b/extensions/jwebot/coin-taffy/tests/test_plugin.py index abca9d7..04d4d4d 100644 --- a/extensions/jwebot/coin-taffy/tests/test_plugin.py +++ b/extensions/jwebot/coin-taffy/tests/test_plugin.py @@ -19,10 +19,10 @@ class PluginTests(unittest.TestCase): for call in context.commands.register.call_args_list ] self.assertEqual(AUTHOR, 'jwebot') - self.assertEqual(DESCRIPTION, 'Taffy金币') - self.assertEqual(HELP_ENTRIES, ('/Taffy金币',)) - self.assertIn('/Taffy金币', registered) - self.assertEqual(len(registered), 2) + self.assertEqual(DESCRIPTION, 'Taffy摩拉') + self.assertEqual(HELP_ENTRIES, ('/Taffy摩拉(兼容 /Taffy金币)',)) + self.assertIn('/Taffy摩拉', registered) + self.assertEqual(len(registered), 3) if __name__ == "__main__": diff --git a/extensions/jwebot/coin-transfer/README.md b/extensions/jwebot/coin-transfer/README.md index 8d5d6e5..ac09559 100644 --- a/extensions/jwebot/coin-transfer/README.md +++ b/extensions/jwebot/coin-transfer/README.md @@ -2,7 +2,7 @@ Author: `jwebot` -金币转账 +摩拉转账 Help: diff --git a/extensions/jwebot/coin-transfer/plugin.toml b/extensions/jwebot/coin-transfer/plugin.toml index 4e8b9f9..e7a8321 100644 --- a/extensions/jwebot/coin-transfer/plugin.toml +++ b/extensions/jwebot/coin-transfer/plugin.toml @@ -1,6 +1,6 @@ id = "coin-transfer" author = "jwebot" -description = "金币转账" +description = "摩拉转账" help = ["/转账 对方 金额;/transfer"] version = "1.0.0" api_version = "1" diff --git a/extensions/jwebot/coin-transfer/src/coin_transfer.py b/extensions/jwebot/coin-transfer/src/coin_transfer.py index 532fa19..1b257cb 100644 --- a/extensions/jwebot/coin-transfer/src/coin_transfer.py +++ b/extensions/jwebot/coin-transfer/src/coin_transfer.py @@ -5,7 +5,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'coin-transfer' AUTHOR = 'jwebot' -DESCRIPTION = '金币转账' +DESCRIPTION = '摩拉转账' HELP_ENTRIES = ('/转账 对方 金额;/transfer',) COMMANDS = ('/转账', '/transfer') diff --git a/extensions/jwebot/coin-transfer/tests/test_plugin.py b/extensions/jwebot/coin-transfer/tests/test_plugin.py index 6a12a24..ff342c1 100644 --- a/extensions/jwebot/coin-transfer/tests/test_plugin.py +++ b/extensions/jwebot/coin-transfer/tests/test_plugin.py @@ -19,7 +19,7 @@ class PluginTests(unittest.TestCase): for call in context.commands.register.call_args_list ] self.assertEqual(AUTHOR, 'jwebot') - self.assertEqual(DESCRIPTION, '金币转账') + self.assertEqual(DESCRIPTION, '摩拉转账') self.assertEqual(HELP_ENTRIES, ('/转账 对方 金额;/transfer',)) self.assertIn('/转账', registered) self.assertEqual(len(registered), 2) diff --git a/extensions/jwebot/single-player-games/README.md b/extensions/jwebot/single-player-games/README.md index 45c7891..d64073b 100644 --- a/extensions/jwebot/single-player-games/README.md +++ b/extensions/jwebot/single-player-games/README.md @@ -2,11 +2,11 @@ Author: `jwebot` -金币游戏 +摩拉游戏 Help: -- `/金币 /wallet;/游戏记录 /gamehistory` +- `/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory` - `/抽奖 [金额];/draw` - `/彩票 开始|报名|状态|开奖|取消;/lottery` - `/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200` @@ -14,6 +14,7 @@ Help: Registered commands: +- `/摩拉` - `/金币` - `/wallet` - `/抽奖` diff --git a/extensions/jwebot/single-player-games/plugin.toml b/extensions/jwebot/single-player-games/plugin.toml index 1aa09d5..7285775 100644 --- a/extensions/jwebot/single-player-games/plugin.toml +++ b/extensions/jwebot/single-player-games/plugin.toml @@ -1,14 +1,14 @@ id = "single-player-games" author = "jwebot" -description = "金币游戏" -help = ["/金币 /wallet;/游戏记录 /gamehistory", "/抽奖 [金额];/draw", "/彩票 开始|报名|状态|开奖|取消;/lottery", "/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200", "/游戏帮助;/games"] +description = "摩拉游戏" +help = ["/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory", "/抽奖 [金额];/draw", "/彩票 开始|报名|状态|开奖|取消;/lottery", "/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200", "/游戏帮助;/games"] version = "1.0.0" api_version = "1" entrypoint = "single_player_games:plugin" type = "command" [capabilities] -commands = ["/金币", "/wallet", "/抽奖", "/draw", "/彩票", "/买彩票", "/lottery", "/21点", "/二十一点", "/blackjack", "/游戏记录", "/个人记录", "/gamehistory", "/游戏帮助", "/games"] +commands = ["/摩拉", "/金币", "/wallet", "/抽奖", "/draw", "/彩票", "/买彩票", "/lottery", "/21点", "/二十一点", "/blackjack", "/游戏记录", "/个人记录", "/gamehistory", "/游戏帮助", "/games"] storage = false network_profiles = [] service_profiles = ["single-player-games"] diff --git a/extensions/jwebot/single-player-games/src/single_player_games.py b/extensions/jwebot/single-player-games/src/single_player_games.py index d783010..b58aa07 100644 --- a/extensions/jwebot/single-player-games/src/single_player_games.py +++ b/extensions/jwebot/single-player-games/src/single_player_games.py @@ -5,9 +5,9 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'single-player-games' AUTHOR = 'jwebot' -DESCRIPTION = '金币游戏' -HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games') -COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games') +DESCRIPTION = '摩拉游戏' +HELP_ENTRIES = ('/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games') +COMMANDS = ('/摩拉', '/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games') def _handler(context: SingleGroupContext, command: str): diff --git a/extensions/jwebot/single-player-games/tests/test_plugin.py b/extensions/jwebot/single-player-games/tests/test_plugin.py index 1da4e6e..29829aa 100644 --- a/extensions/jwebot/single-player-games/tests/test_plugin.py +++ b/extensions/jwebot/single-player-games/tests/test_plugin.py @@ -19,10 +19,10 @@ class PluginTests(unittest.TestCase): for call in context.commands.register.call_args_list ] self.assertEqual(AUTHOR, 'jwebot') - self.assertEqual(DESCRIPTION, '金币游戏') - self.assertEqual(HELP_ENTRIES, ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games')) - self.assertIn('/金币', registered) - self.assertEqual(len(registered), 15) + self.assertEqual(DESCRIPTION, '摩拉游戏') + self.assertEqual(HELP_ENTRIES, ('/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games')) + self.assertIn('/摩拉', registered) + self.assertEqual(len(registered), 16) if __name__ == "__main__": diff --git a/packages/jwebot-config/pyproject.toml b/packages/jwebot-config/pyproject.toml index 133b114..59034f0 100644 --- a/packages/jwebot-config/pyproject.toml +++ b/packages/jwebot-config/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-config" -version = "1.0.2b1+1016" +version = "1.0.3b1+1016" description = "Profile-based configuration compiler for JWeBot" requires-python = ">=3.9" dependencies = [ diff --git a/packages/jwebot-config/src/jwebot_config/__init__.py b/packages/jwebot-config/src/jwebot_config/__init__.py index d2ced4f..286c7f1 100644 --- a/packages/jwebot-config/src/jwebot_config/__init__.py +++ b/packages/jwebot-config/src/jwebot_config/__init__.py @@ -36,4 +36,4 @@ __all__ = [ "write_default_profile", ] -__version__ = "1.0.2b1+1016" +__version__ = "1.0.3b1+1016" diff --git a/packages/jwebot-extension-host/pyproject.toml b/packages/jwebot-extension-host/pyproject.toml index e93d95e..3a9cbd7 100644 --- a/packages/jwebot-extension-host/pyproject.toml +++ b/packages/jwebot-extension-host/pyproject.toml @@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-extension-host" -version = "1.0.2b1+1016" +version = "1.0.3b1+1016" description = "Single-release, single-invocation process host for JWeBot extensions" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.0.2b1+1016", - "jwebot-kernel==1.0.2b1+1016", + "jwebot-sdk==1.0.3b1+1016", + "jwebot-kernel==1.0.3b1+1016", ] [project.scripts] diff --git a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py index 5f87c1d..c1ed230 100644 --- a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py +++ b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py @@ -16,4 +16,4 @@ __all__ = [ "process_request", ] -__version__ = "1.0.2b1+1016" +__version__ = "1.0.3b1+1016" diff --git a/packages/jwebot-kernel/pyproject.toml b/packages/jwebot-kernel/pyproject.toml index 70a50d9..835dcfc 100644 --- a/packages/jwebot-kernel/pyproject.toml +++ b/packages/jwebot-kernel/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-kernel" -version = "1.0.2b1+1016" +version = "1.0.3b1+1016" description = "Stable routing and extension authority for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-config==1.0.2b1+1016", - "jwebot-sdk==1.0.2b1+1016", - "jwebot-official-extensions==1.0.2b1+1016", + "jwebot-config==1.0.3b1+1016", + "jwebot-sdk==1.0.3b1+1016", + "jwebot-official-extensions==1.0.3b1+1016", "tomli>=2; python_version < '3.11'", ] diff --git a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py index 40f83fa..82e6da6 100644 --- a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py +++ b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py @@ -107,4 +107,4 @@ __all__ = [ "verify_installed_release_signature", ] -__version__ = "1.0.2b1+1016" +__version__ = "1.0.3b1+1016" diff --git a/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py b/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py index 6dcf4cd..9d87ef3 100644 --- a/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py +++ b/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py @@ -71,6 +71,8 @@ from jwebot_official_extensions.steam import ( SteamUnavailable, ) from jwebot_official_extensions.player_games import ( + append_coin_event, + append_game_coin_event, append_game_record, hand_value, next_game_index, @@ -98,6 +100,7 @@ EXTENSION_SOCKET_ROOT = Path("/run/jwebot-extension-host") MAX_EVENT_TEXT = 5_000 MAX_REPLY_CHARS = 900 MAX_CONTEXT_TURNS = 6 +OCR_EVENT_SOURCES = frozenset({"ocr", "rapidocr-local"}) MAX_OUTBOX_ATTEMPTS = 3 DIRECT_CHAT_GENERATION_WORKERS = 2 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) +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]: decision = route_question(question) return decision.route.value, decision.reason @@ -1293,6 +1301,43 @@ class Ledger: raise EdgeError("ledger write did not persist") 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( self, event_id: str, @@ -3140,7 +3185,7 @@ class Ledger: def _player_identity(self, group: str, sender: str) -> dict[str, str]: if not group or not sender or sender == "unknown": - raise LocalCommandError("无法确认本人身份,不能操作金币账户") + raise LocalCommandError("无法确认本人身份,不能操作摩拉账户") person = self.resolve_person(group, sender) person["display_name"] = person_display_label(person["display_name"]) return person @@ -3178,13 +3223,15 @@ class Ledger: (group, person_key), ).fetchone() if stored is None: - return normalize_player_account({}) + account = normalize_player_account({}) + append_coin_event(account, int(account["balance"]), "首次初始化") + return account try: account = json.loads(str(stored["state_json"])) except json.JSONDecodeError as error: - raise LocalCommandError("金币账户状态无效") from error + raise LocalCommandError("摩拉账户状态无效") from error if not isinstance(account, dict): - raise LocalCommandError("金币账户状态无效") + raise LocalCommandError("摩拉账户状态无效") return normalize_player_account(account) def _save_player_account( @@ -3199,7 +3246,7 @@ class Ledger: type(account["balance"]) is not int or not 0 <= account["balance"] <= 1_000_000_000 ): - raise LocalCommandError("金币结算超出允许范围") + raise LocalCommandError("摩拉结算超出允许范围") self.connection.execute( """ INSERT INTO player_accounts VALUES (?, ?, ?, ?) @@ -3555,6 +3602,13 @@ class Ledger: + " ".join(map(str, drawn_numbers)) ), ) + append_game_coin_event( + account, + kind="lottery", + outcome=f"命中{matches}个", + stake=stake, + payout=payout, + ) self._save_player_account( group, key, account, timestamp ) @@ -3661,6 +3715,13 @@ class Ledger: payout=stake, summary="主持人取消,下注已全额退回", ) + append_game_coin_event( + account, + kind="lottery", + outcome="本轮取消", + stake=stake, + payout=stake, + ) self._save_player_account( group, key, account, timestamp ) @@ -3827,6 +3888,13 @@ class Ledger: 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.connection.execute( """ @@ -4830,9 +4898,9 @@ class Ledger: try: account = json.loads(str(row["state_json"])) except json.JSONDecodeError as error: - raise LocalCommandError("金币账户状态无效") from error + raise LocalCommandError("摩拉账户状态无效") from error if not isinstance(account, dict): - raise LocalCommandError("金币账户状态无效") + raise LocalCommandError("摩拉账户状态无效") normalize_player_account(account) loan = account.get("loan") loan_principal = ( @@ -4939,9 +5007,9 @@ class Ledger: try: account = json.loads(str(row["state_json"])) except json.JSONDecodeError as error: - raise LocalCommandError("金币账户状态无效") from error + raise LocalCommandError("摩拉账户状态无效") from error if not isinstance(account, dict): - raise LocalCommandError("金币账户状态无效") + raise LocalCommandError("摩拉账户状态无效") normalize_player_account(account) stake = 0 payout = 0 @@ -5052,6 +5120,16 @@ class Ledger: ) source_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() self._save_player_account( group, source_key, source_account, timestamp @@ -6502,6 +6580,34 @@ class EdgeApplication: reason=reason, 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( event, question=question, diff --git a/packages/jwebot-official-extensions/pyproject.toml b/packages/jwebot-official-extensions/pyproject.toml index b10353b..601db22 100644 --- a/packages/jwebot-official-extensions/pyproject.toml +++ b/packages/jwebot-official-extensions/pyproject.toml @@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-official-extensions" -version = "1.0.2b1+1016" +version = "1.0.3b1+1016" description = "Reviewed local command adapters and broker implementation for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.0.2b1+1016", + "jwebot-sdk==1.0.3b1+1016", ] [tool.setuptools.packages.find] diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py index 6d2ede9..3602327 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py @@ -90,4 +90,4 @@ __all__ = [ "render_feature_list", ] -__version__ = "1.0.2b1+1016" +__version__ = "1.0.3b1+1016" diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_ranking.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_ranking.py index 938f8aa..9374bab 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_ranking.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_ranking.py @@ -7,7 +7,7 @@ PROFILE = 'coin-ranking' AUTHOR = 'jwebot' DESCRIPTION = '富榜' HELP_ENTRIES = ('/大富翁榜',) -COMMANDS = ('/金币排行', '/金币排行榜', '/大富翁榜', '/richlist') +COMMANDS = ('/摩拉排行', '/摩拉排行榜', '/金币排行', '/金币排行榜', '/大富翁榜', '/richlist') def _handler(context: SingleGroupContext, command: str): diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_taffy.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_taffy.py index 08974dd..0b1836a 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_taffy.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_taffy.py @@ -5,9 +5,9 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'coin-taffy' AUTHOR = 'jwebot' -DESCRIPTION = 'Taffy金币' -HELP_ENTRIES = ('/Taffy金币',) -COMMANDS = ('/Taffy金币', '/taffycoins') +DESCRIPTION = 'Taffy摩拉' +HELP_ENTRIES = ('/Taffy摩拉(兼容 /Taffy金币)',) +COMMANDS = ('/Taffy摩拉', '/Taffy金币', '/taffycoins') def _handler(context: SingleGroupContext, command: str): diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_transfer.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_transfer.py index 532fa19..1b257cb 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_transfer.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/coin_transfer.py @@ -5,7 +5,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'coin-transfer' AUTHOR = 'jwebot' -DESCRIPTION = '金币转账' +DESCRIPTION = '摩拉转账' HELP_ENTRIES = ('/转账 对方 金额;/transfer',) COMMANDS = ('/转账', '/transfer') diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/single_player_games.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/single_player_games.py index d783010..b58aa07 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/single_player_games.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/single_player_games.py @@ -5,9 +5,9 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'single-player-games' AUTHOR = 'jwebot' -DESCRIPTION = '金币游戏' -HELP_ENTRIES = ('/金币 /wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games') -COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games') +DESCRIPTION = '摩拉游戏' +HELP_ENTRIES = ('/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games') +COMMANDS = ('/摩拉', '/金币', '/wallet', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏记录', '/个人记录', '/gamehistory', '/游戏帮助', '/games') def _handler(context: SingleGroupContext, command: str): diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/cs_autochess.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/cs_autochess.py index c18f660..f332f91 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/cs_autochess.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/cs_autochess.py @@ -956,7 +956,7 @@ def _shop_text(state: dict[str, Any], player: dict[str, Any]) -> str: lines = [ ( 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"阵容 {power['power']:.1f};商店{lock}" ) @@ -1143,7 +1143,7 @@ def _auto_manage_ai( ) text = ( f"{player['display_name']} 自动运营:{'、'.join(actions)};" - f"金币 {player['gold']},五人首发战力 " + f"摩拉 {player['gold']},五人首发战力 " f"{lineup_power(player)['power']:.1f}。" ) stories.append( @@ -1571,7 +1571,7 @@ def _settle_pve( ) text = ( 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)) 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)}级" ) 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"阵容 {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)} +{TRAINING_WIN_GOLD}," + f"摩拉:{_team_name(winner)} +{TRAINING_WIN_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 ( 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: intro = ( f"欢迎来到{state['event_title']}!你们已经到达酒店宾馆," - f"训练赛共 {len(rounds)} 轮。训练赛不扣生命,只产出运营金币。" + f"训练赛共 {len(rounds)} 轮。训练赛不扣生命,只产出运营摩拉。" ) parts.append(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 final_text = ( 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 或锁店等下一轮运营。" ) parts[-1] = parts[-1] + "\n" + final_text @@ -2745,7 +2745,7 @@ def _special_event_parts( ) text = ( f"{_team_name(player)} {'完成' if won else '勉强应付'}{event_kind}:" - f"{hook}获得 {reward} 金币。" + f"{hook}获得 {reward} 摩拉。" ) parts.append(text) 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}) return respond(state, text, stories=[story]) if action == "coach": - return respond(state, "教练现在由第一轮选秀决定,本届不能用金币升级教练。") + return respond(state, "教练现在由第一轮选秀决定,本届不能用摩拉升级教练。") if action == "buy": values = _parse_indices(arguments) if len(values) != 1 or not 1 <= values[0] <= SHOP_SIZE: @@ -3152,7 +3152,7 @@ class CsMajorAutochess: return respond(state, "这个商店位置已经购买或没有候选") card = CARD_BY_ID[str(card_id)] 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( unit["card_id"] == card_id for unit in [*player["board"], *player["bench"]] @@ -3166,7 +3166,7 @@ class CsMajorAutochess: star, upgraded = _merge_purchase(player, str(card_id)) player["shop"][slot] = None text = ( - f"{display_name} 签下 {card['name']},花费 {card['cost']} 金币;" + f"{display_name} 签下 {card['name']},花费 {card['cost']} 摩拉;" f"当前 {star} 星,余额 {player['gold']}。" ) if upgraded: @@ -3175,7 +3175,7 @@ class CsMajorAutochess: return respond(state, text, stories=[story]) if action == "refresh": 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["shop"] = _draw_shop( state, @@ -3222,7 +3222,7 @@ class CsMajorAutochess: refund = int(card["cost"]) * int(unit["copies"]) player["gold"] += refund 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 person_key not in state.get("ready_keys", []): diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/data/cs_autochess/narratives.json b/packages/jwebot-official-extensions/src/jwebot_official_extensions/data/cs_autochess/narratives.json index 3ec0f8b..680a6eb 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/data/cs_autochess/narratives.json +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/data/cs_autochess/narratives.json @@ -11,7 +11,7 @@ "队伍如果阵容完整,就更容易把这次插曲处理成奖金。", "这次挑战只出现一次,处理完就回到小组赛主线。", "主舞台彩排占用了热身时间,教练的管理能力开始生效。", - "赞助商要求展示新战术,选错方向也不会淘汰,只会少拿金币。", + "赞助商要求展示新战术,选错方向也不会淘汰,只会少拿摩拉。", "现场采访提到了前一场失利,队员心态需要稳住。", "这类挑战不扣生命,目的只是给运营阶段增加变数。", "处理好这次插曲的队伍,会在下一场前多一点经济余地。" @@ -141,21 +141,21 @@ "{team} 这波 {count} 连分让地图气质变了,所有枪位都开始往前压。" ], "training_hooks": [ - "训练赛不计入小组胜负,但会发金币,方便后续刷新商店。", + "训练赛不计入小组胜负,但会发摩拉,方便后续刷新商店。", "这轮训练更像实战热身,每队都会遇到同场参赛对手。", "训练赛重点不是淘汰谁,而是检验当前阵容能不能打完整张图。", "这轮结束后可以 D 牌、锁店或调首发,下一轮再检验成果。", "AI 战队也会参与训练,对局结果会写入档案。", "如果阵容缺少指挥,训练赛的中期回合会更容易失控。", "如果你想追羁绊,现在是最适合刷新商店的窗口。", - "训练赛的金币很重要,别把每次刷新都当成免费试错。", + "训练赛的摩拉很重要,别把每次刷新都当成免费试错。", "这一阶段没有生命扣除,重点是运营和阵容成型。", "训练赛的地图会轮换,不会一直用同一个固定比分模板。", "每轮训练结束后,真人都需要发送 /结束回合 才会进入下一轮。", "训练赛会按轮次播报,避免一次性刷出一大串结果。", "如果出现轮空,会安排本地陪练队补一张 BO1。", "训练阶段适合观察战术克制,火力、稳定、残局都会影响回合走向。", - "连胜会带来金币,但失败也有保底,保证测试局能继续运营。", + "连胜会带来摩拉,但失败也有保底,保证测试局能继续运营。", "这部分是纯文字自走棋的节奏层,不再是单条目标数值判定。" ] }, diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/descriptors.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/descriptors.py index df06a80..a02d09d 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/descriptors.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/descriptors.py @@ -40,12 +40,16 @@ LOCAL_COMMAND_ALIASES = { "steam": "steam_friends", "上下文": "context_summary", "context": "context_summary", + "摩拉": "wallet", "金币": "wallet", "wallet": "wallet", + "摩拉排行": "coin_ranking", + "摩拉排行榜": "coin_ranking", "金币排行": "coin_ranking", "金币排行榜": "coin_ranking", "大富翁榜": "coin_ranking", "richlist": "coin_ranking", + "Taffy摩拉": "taffy_coins", "Taffy金币": "taffy_coins", "taffycoins": "taffy_coins", "转账": "transfer", @@ -152,9 +156,9 @@ LOCAL_EXTENSIONS = ( LocalExtensionDescriptor( extension_id="single-player-games", author=JWEBOT_AUTHOR, - description="金币游戏", + description="摩拉游戏", help_entries=( - "/金币 /wallet;/游戏记录 /gamehistory", + "/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory", "/抽奖 [金额];/draw", "/彩票 开始|报名|状态|开奖|取消;/lottery", "/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200", @@ -172,7 +176,7 @@ LOCAL_EXTENSIONS = ( LocalExtensionDescriptor( extension_id="coin-transfer", author=JWEBOT_AUTHOR, - description="金币转账", + description="摩拉转账", help_entries=("/转账 对方 金额;/transfer",), command_kinds=("transfer",), ), @@ -186,8 +190,8 @@ LOCAL_EXTENSIONS = ( LocalExtensionDescriptor( extension_id="coin-taffy", author=JWEBOT_AUTHOR, - description="Taffy金币", - help_entries=("/Taffy金币",), + description="Taffy摩拉", + help_entries=("/Taffy摩拉(兼容 /Taffy金币)",), command_kinds=("taffy_coins",), ), LocalExtensionDescriptor( diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/local_commands.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/local_commands.py index 7ba3ae4..764cb3d 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/local_commands.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/local_commands.py @@ -365,7 +365,7 @@ class LocalCommandEngine: return (f"人物:{self.person_display_label(person['display_name'])};" f"群内索引 {person['person_key'][:12]};别名:{aliases or '未配置'}。\n" f"基本属性:{facts or '暂无配置'}。\n" - "本人金币 /金币;个人记忆 /我的记忆。身份由本地昵称映射解析,无法读取微信原生账号ID。") + "本人摩拉 /摩拉;个人记忆 /我的记忆。身份由本地昵称映射解析,无法读取微信原生账号ID。") @staticmethod def service_state(service_name: str) -> str: diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/player_games.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/player_games.py index 2d08e39..4d3f2b8 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/player_games.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/player_games.py @@ -26,6 +26,8 @@ MAX_LOAN_AMOUNT = 500 GAME_TIMEOUT_SECONDS = 600 GAME_HISTORY_LIMIT = 50 GAME_HISTORY_PAGE_SIZE = 5 +COIN_EVENT_LIMIT = 50 +COIN_EVENT_DISPLAY_LIMIT = 10 GAME_LABELS = { "prize_draw": "抽奖", "lottery": "彩票", @@ -54,11 +56,48 @@ BLACKJACK_TABLE_BET = re.compile( ) TRANSFER_MARKED = re.compile( r"^(?:给|to)?\s*(?P.+?)\s*" - r"(?:金额|金币|coin|coins|:|:)\s*(?P\d{1,6})$", + r"(?:金额|摩拉|金币|coin|coins|:|:)\s*(?P\d{1,6})$", 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]: account.setdefault("balance", INITIAL_COINS) account.setdefault("game", None) @@ -90,6 +129,33 @@ def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]: account["game_history"] = [ entry for entry in history if isinstance(entry, dict) ][-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 @@ -133,6 +199,40 @@ def append_game_record( 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: return ("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K")[card % 13] @@ -155,10 +255,11 @@ class PlayerGames: @staticmethod def rules() -> str: return ( - "每人首次使用获 1000 虚拟金币;/金币 查看余额;/游戏记录 查看本人记录。\n" + "每人首次使用获 1000 虚拟摩拉;/摩拉 查看余额(兼容 /金币);" + "/游戏记录 查看本人记录。\n" "转账:/转账 对方 金额,或 /transfer name amount;" - "对方需先用 /金币 初始化账户。\n" - "贷款:/贷款 [1~500] 借入金币;/还款 [金额|全部] 归还," + "对方需先用 /摩拉 或 /金币 初始化账户。\n" + "贷款:/贷款 [1~500] 借入摩拉;/还款 [金额|全部] 归还," "同一时间最多一笔未还贷款。\n" "抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n" "多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];" @@ -172,7 +273,7 @@ class PlayerGames: "主持人 /21点 多人 开始 后按提示 /21点 放200" "(1~10000);轮到自己 /21点 要牌|停牌。\n" "21点获胜返2倍、平局返下注、自然21点返2.5倍向下取整,庄家软17停牌。" - "以上均含本金;虚拟金币不能充值或兑换。" + "以上均含本金;虚拟摩拉不能充值或兑换。" ) def handle(self, row: dict[str, Any], kind: str, arguments: str) -> str: @@ -194,10 +295,10 @@ class PlayerGames: if kind in {"loan", "repay"}: return self.loan_or_repay(row, kind, arguments) if kind == "wallet" and arguments: - return "用法:/金币(/wallet),查看本人的虚拟金币;/游戏帮助 查看规则" + return "用法:/摩拉(兼容 /金币、/wallet),查看本人的虚拟摩拉;/游戏帮助 查看规则" if kind == "prize_draw": 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) elif kind == "blackjack": table_command = self.parse_blackjack_table_command(arguments) @@ -255,13 +356,13 @@ class PlayerGames: else: bet = 0 if kind == "prize_draw" and not MIN_DRAW_BET <= bet <= MAX_DRAW_BET: - return "抽奖下注必须为 2~100 的整数金币" + return "抽奖下注必须为 2~100 的整数摩拉" if ( kind == "blackjack" and action == "start" and not MIN_BLACKJACK_BET <= bet <= MAX_BLACKJACK_BET ): - return "21点下注必须为 1~500 的整数金币" + return "21点下注必须为 1~500 的整数摩拉" person = self.ledger.resolve_person( str(row["group_fingerprint"]), str(row["sender"]), @@ -291,6 +392,13 @@ class PlayerGames: payout=0, summary="10分钟无有效操作,按认输结束", ) + append_game_coin_event( + account, + kind="blackjack", + outcome="超时认输", + stake=int(game["bet"]), + payout=0, + ) account["game"] = None account["losses"] += 1 expired = "上一局已超时,按认输结束;" @@ -298,11 +406,20 @@ class PlayerGames: state = "进行中" if account.get("game") else "无进行中的牌局" loan = account.get("loan") loan_text = f"未还贷款 {loan['principal']};" if loan else "无贷款;" - return ( - f"{expired}金币 {account['balance']};{loan_text}21点 {state};" - f"已结算胜 {account['wins']} / 负 {account['losses']} / 平 {account['ties']}。" - "首次赠送1000仅发一次;/游戏帮助 查看规则" - ) + total = account["wins"] + account["losses"] + account["ties"] + win_rate = (account["wins"] / total * 100) if total else 0.0 + lines = [ + 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"}: active = account.get("game") index = int(active["index"]) if active else None @@ -318,7 +435,7 @@ class PlayerGames: else message ) 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 account["balance"] -= bet if kind == "blackjack": @@ -350,10 +467,17 @@ class PlayerGames: payout=payout, summary=f"抽奖结果 {result}", ) + append_game_coin_event( + account, + kind="prize_draw", + outcome=result, + stake=bet, + payout=payout, + ) return player_reply( expired + f"{result};下注 {bet},返还 {payout}," - f"净变化 {payout - bet:+d};金币 {account['balance']}", + f"净变化 {payout - bet:+d};摩拉 {account['balance']}", index, ) @@ -364,22 +488,22 @@ class PlayerGames: def coin_ranking(self, row: dict[str, Any], arguments: str) -> str: value = arguments.strip() if value and not re.fullmatch(r"\d{1,2}", value): - return "用法:/大富翁榜 或 /金币排行 [人数]" + return "用法:/大富翁榜、/摩拉排行 [人数] 或 /金币排行 [人数]" limit = int(value or 10) if not 1 <= limit <= 20: - return "金币排行人数必须为 1~20" + return "摩拉排行人数必须为 1~20" records = self.ledger.player_coin_ranking( str(row["group_fingerprint"]), limit=limit, ) if not records: - return "当前群还没有金币账户;发送 /金币 初始化" + return "当前群还没有摩拉账户;发送 /摩拉 或 /金币 初始化" lines = ["大富翁榜:"] for index, record in enumerate(records, start=1): loan = int(record.get("loan", 0)) loan_text = f"(未还贷款 {loan})" if loan else "" lines.append( - f"{index}. {record['display_name']} {record['balance']} 金币" + f"{index}. {record['display_name']} {record['balance']} 摩拉" f"{loan_text}" ) return "\n".join(lines) @@ -387,21 +511,21 @@ class PlayerGames: def taffy_coins(self, row: dict[str, Any], arguments: str) -> str: value = arguments.strip() if value and not re.fullmatch(r"\d{1,2}", value): - return "用法:/Taffy金币 [人数]" + return "用法:/Taffy摩拉 [人数](兼容 /Taffy金币)" limit = int(value or 8) if not 1 <= limit <= 20: - return "Taffy金币人数必须为 1~20" + return "Taffy摩拉人数必须为 1~20" stats = self.ledger.player_taffy_coin_stats( str(row["group_fingerprint"]), limit=limit, ) if int(stats["records"]) == 0: - return "Taffy金币:当前群暂无已结算游戏记录" + return "Taffy摩拉:当前群暂无已结算游戏记录" 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 = [ ( - f"Taffy金币:已结算 {stats['records']} 局;" + f"Taffy摩拉:已结算 {stats['records']} 局;" f"玩家总下注 {stats['stake']},总返还 {stats['payout']};" f"{result}" ) @@ -551,7 +675,7 @@ class PlayerGames: ) if action == "bet": if not MIN_BLACKJACK_TABLE_BET <= amount <= MAX_BLACKJACK_TABLE_BET: - return "21点多人牌桌筹码必须为 1~10000 的整数金币" + return "21点多人牌桌筹码必须为 1~10000 的整数摩拉" deck = list(range(52)) self.rng.shuffle(deck) return self.ledger.place_group_blackjack_bet( @@ -710,13 +834,13 @@ class PlayerGames: return self.blackjack_prompt(data["prompt_player"]) if status == "insufficient": return ( - f"{data['display_name']}:金币不足,余额 {data['balance']}," + f"{data['display_name']}:摩拉不足,余额 {data['balance']}," f"本次需要 {data['bet']}" ) if status == "bet_accepted": return ( f"{data['display_name']} 放下 {data['bet']};" - f"金币 {data['balance']}\n" + f"摩拉 {data['balance']}\n" f"{self.blackjack_prompt(data['prompt_player'])}" ) if status == "dealt": @@ -814,7 +938,7 @@ class PlayerGames: f"{player['display_name']} #{player['player_game_index']}:" f"{player['outcome']};牌 {self.blackjack_hand_text(player['hand'])};" f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};" - f"金币 {player['balance']}" + f"摩拉 {player['balance']}" ) return "\n".join(lines) @@ -864,11 +988,11 @@ class PlayerGames: return ( "用法:/转账 对方 金额,例如 /转账 Bob 100;" "英文 /transfer Bob 100。金额范围 1~100000," - "对方需先发送 /金币 初始化账户" + "对方需先发送 /摩拉 或 /金币 初始化账户" ) target, amount = parsed if not MIN_TRANSFER_AMOUNT <= amount <= MAX_TRANSFER_AMOUNT: - return "转账金额必须为 1~100000 的整数金币" + return "转账金额必须为 1~100000 的整数摩拉" def render(data: dict[str, Any]) -> str: source = data["source_display_name"] @@ -877,18 +1001,18 @@ class PlayerGames: return f"{source}:不能给自己转账" if data["status"] == "target_missing": return ( - f"{source}:{target_name} 还没有金币账户," - "请先让对方发送 /金币 初始化" + f"{source}:{target_name} 还没有摩拉账户," + "请先让对方发送 /摩拉 或 /金币 初始化" ) if data["status"] == "insufficient": return ( - f"{source}:金币不足,余额 {data['source_balance']}," + f"{source}:摩拉不足,余额 {data['source_balance']}," f"本次需要 {data['amount']}" ) return ( - f"{source}:已向 {target_name} 转账 {data['amount']} 金币;" - f"本人金币 {data['source_balance']};" - f"{target_name} 金币 {data['target_balance']}" + f"{source}:已向 {target_name} 转账 {data['amount']} 摩拉;" + f"本人摩拉 {data['source_balance']};" + f"{target_name} 摩拉 {data['target_balance']}" ) return self.ledger.transfer_player_coins( @@ -908,7 +1032,7 @@ class PlayerGames: return "用法:/贷款 [1~500];同一时间最多一笔未还贷款" amount = int(value) if not MIN_LOAN_AMOUNT <= amount <= MAX_LOAN_AMOUNT: - return "贷款金额必须为 1~500 的整数金币" + return "贷款金额必须为 1~500 的整数摩拉" else: amount = 0 if value not in {"", "全部", "all"}: @@ -916,7 +1040,7 @@ class PlayerGames: return "用法:/还款 [金额|全部]" amount = int(value) if amount <= 0: - return "还款金额必须为正整数金币" + return "还款金额必须为正整数摩拉" group = str(row["group_fingerprint"]) sender = str(row["sender"]) @@ -929,7 +1053,7 @@ class PlayerGames: if kind == "loan": if loan: return ( - f"{player_label}:已有未还贷款 {loan['principal']} 金币;" + f"{player_label}:已有未还贷款 {loan['principal']} 摩拉;" "请先 /还款 [金额|全部]" ) account["balance"] += amount @@ -937,9 +1061,10 @@ class PlayerGames: "principal": amount, "borrowed_at": time.time(), } + append_coin_event(account, amount, "贷款到账") return ( - f"{player_label}:贷款 {amount} 金币已到账;" - f"当前金币 {account['balance']};未还贷款 {amount}" + f"{player_label}:贷款 {amount} 摩拉已到账;" + f"当前摩拉 {account['balance']};未还贷款 {amount}" ) if not loan: return f"{player_label}:当前没有未还贷款" @@ -949,13 +1074,14 @@ class PlayerGames: else: pay = min(amount, debt) if pay <= 0: - return f"{player_label}:金币不足,无法还款;未还贷款 {debt}" + return f"{player_label}:摩拉不足,无法还款;未还贷款 {debt}" if pay > int(account["balance"]): return ( - f"{player_label}:金币不足,余额 {account['balance']}," + f"{player_label}:摩拉不足,余额 {account['balance']}," f"本次还款需要 {pay}" ) account["balance"] -= pay + append_coin_event(account, -pay, "还款") debt -= pay if debt: account["loan"] = { @@ -963,13 +1089,13 @@ class PlayerGames: "borrowed_at": float(loan.get("borrowed_at", time.time())), } return ( - f"{player_label}:已还款 {pay} 金币;" - f"剩余贷款 {debt};当前金币 {account['balance']}" + f"{player_label}:已还款 {pay} 摩拉;" + f"剩余贷款 {debt};当前摩拉 {account['balance']}" ) account["loan"] = None return ( - f"{player_label}:已还清贷款 {pay} 金币;" - f"当前金币 {account['balance']}" + f"{player_label}:已还清贷款 {pay} 摩拉;" + f"当前摩拉 {account['balance']}" ) return self.ledger.apply_player_action( @@ -1035,7 +1161,7 @@ class PlayerGames: if len(set(digits)) != 3: raise ValueError("彩票的三个数字必须互不重复") if not MIN_LOTTERY_BET <= stake <= MAX_LOTTERY_BET: - raise ValueError("彩票下注必须为 1~500 的整数金币") + raise ValueError("彩票下注必须为 1~500 的整数摩拉") return tuple(sorted(digits)), stake def group_lottery(self, row: dict[str, Any], arguments: str) -> str: @@ -1106,7 +1232,7 @@ class PlayerGames: f"命中 {entry['matches']} 个;下注 {entry['stake']}," f"返还 {entry['payout']},净变化 " f"{entry['payout'] - entry['stake']:+d};" - f"金币 {entry['balance']}" + f"摩拉 {entry['balance']}" ) return "\n".join(lines) @@ -1154,7 +1280,7 @@ class PlayerGames: return "当前没有进行中的彩票;先发送 /彩票 开始" if data["status"] == "insufficient": return ( - f"{data['display_name']}:金币不足,余额 {data['balance']}," + f"{data['display_name']}:摩拉不足,余额 {data['balance']}," f"报名需要 {stake}" ) action = "报名已更新" if data["status"] == "updated" else "报名成功" @@ -1162,7 +1288,7 @@ class PlayerGames: f"{data['display_name']} #{data['player_game_index']}:" f"彩票第 {data['round_number']} 把{action};号码 " f"{' '.join(map(str, data['numbers']))};下注 {data['stake']};" - f"金币 {data['balance']}" + f"摩拉 {data['balance']}" ) return self.ledger.register_group_lottery( @@ -1227,9 +1353,16 @@ class PlayerGames: payout=0, summary="主动结束牌局,不退下注", ) + append_game_coin_event( + account, + kind="blackjack", + outcome="主动认输", + stake=bet, + payout=0, + ) account["game"] = None account["losses"] += 1 - return f"本人的21点已认输结束,不退下注;金币 {account['balance']}" + return f"本人的21点已认输结束,不退下注;摩拉 {account['balance']}" game["updated_at"] = time.time() if action == "hit": game["player"].append(game["deck"].pop()) @@ -1269,7 +1402,14 @@ class PlayerGames: f"玩家{player}点,庄家{dealer}点" ), ) + append_game_coin_event( + account, + kind="blackjack", + outcome=outcome, + stake=int(bet), + payout=int(payout), + ) account["game"] = None return (f"21点结算:{outcome};你的牌 {' '.join(map(card_label, game['player']))}({player}点)," f"庄家 {' '.join(map(card_label, game['dealer']))}({dealer}点);" - f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};金币 {account['balance']}") + f"下注 {bet},返还 {payout},净变化 {payout - bet:+d};摩拉 {account['balance']}") diff --git a/packages/jwebot-sdk/pyproject.toml b/packages/jwebot-sdk/pyproject.toml index 8409651..ab18857 100644 --- a/packages/jwebot-sdk/pyproject.toml +++ b/packages/jwebot-sdk/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-sdk" -version = "1.0.2b1+1016" +version = "1.0.3b1+1016" description = "Typed, capability-limited extension API for JWeBot plugins" requires-python = ">=3.9" dependencies = [] diff --git a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py index 6afe6ce..a10f4bb 100644 --- a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py +++ b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py @@ -38,4 +38,4 @@ __all__ = [ "SingleGroupContext", ] -__version__ = "1.0.2b1+1016" +__version__ = "1.0.3b1+1016" diff --git a/pyproject.toml b/pyproject.toml index 61846b4..88101d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.jwebot] -release = "beta 1.0.2.1016" +release = "beta 1.0.3.1016" production_deployments_enabled_by_default = false [tool.jwebot.workspace] diff --git a/tests/test_jwebot_edge.py b/tests/test_jwebot_edge.py index 3be9b0c..272a9e3 100644 --- a/tests/test_jwebot_edge.py +++ b/tests/test_jwebot_edge.py @@ -677,6 +677,23 @@ class LedgerAndApplicationTests(unittest.TestCase): self.application.deepseek.complete.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): self.application.deepseek.complete = mock.Mock(return_value="最终回复喵") self.application.deliver = mock.Mock() @@ -867,9 +884,9 @@ class LedgerAndApplicationTests(unittest.TestCase): self.assertEqual(version["route"], "local") self.assertIn("JWeBot 版本", 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("更新摘要:Cache OCR-negative", version["text"]) + self.assertIn("更新摘要:Rename the virtual currency", version["text"]) self.assertEqual(debug["route"], "local") self.assertIn("调试埋点", debug["text"]) self.assertEqual(balance["route"], "local") diff --git a/tests/test_release_061.py b/tests/test_release_061.py index 8fdd8bf..2eec520 100644 --- a/tests/test_release_061.py +++ b/tests/test_release_061.py @@ -39,7 +39,7 @@ class Release061Tests(unittest.TestCase): return json.loads(row[0]) def test_aliases_are_corresponding_and_case_insensitive(self): - pairs = [("上下文", "Context"), ("金币", "wallet"), ("抽奖", "draw"), + pairs = [("上下文", "Context"), ("摩拉", "wallet"), ("抽奖", "draw"), ("买彩票", "lottery"), ("21点", "blackjack"), ("人物列表", "people"), ("人物", "person"), ("游戏帮助", "games")] for chinese, english in pairs: @@ -82,18 +82,18 @@ class Release061Tests(unittest.TestCase): self.assertEqual(ledger.search_repeat_history("group-a", "好"), []) 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): first = self.command("/draw 10", event_id="draw-1") replay = self.command("/draw 10", sender="a1", event_id="draw-1") self.assertEqual(first, replay) self.assertEqual(self.state("a1")["balance"], 990) - self.assertIn("金币 1000", self.command("/wallet", sender="Bob")) - self.assertIn("金币 1000", self.command("/wallet", group="group-b")) + self.assertIn("摩拉 1000", self.command("/wallet", sender="Bob")) + self.assertIn("摩拉 1000", self.command("/wallet", group="group-b")) self.engine.ledger.connection.close() self.engine = MODULE.EdgeApplication(config(self.root)) 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): replies = [] @@ -123,7 +123,7 @@ class Release061Tests(unittest.TestCase): with mock.patch.object(self.local.players.rng, "randrange", return_value=99): for _ in range(10): self.command("/draw 100") - self.assertIn("金币不足", self.command("/draw 2")) + self.assertIn("摩拉不足", self.command("/draw 2")) self.assertEqual(self.state()["balance"], 0) def test_blackjack_is_private_persistent_and_settles_exactly_once(self): diff --git a/tests/test_release_062.py b/tests/test_release_062.py index 1f97ded..7d7ce03 100644 --- a/tests/test_release_062.py +++ b/tests/test_release_062.py @@ -119,8 +119,8 @@ class Release062BlackjackTests(unittest.TestCase): self.assertIn("分两步", reply) self.assertIn("100 是本局下注", reply) self.assertIn("再单独发送", reply) - wallet = self.command("/金币") - self.assertIn("金币 1000", wallet) + wallet = self.command("/摩拉") + self.assertIn("摩拉 1000", wallet) self.assertIn("无进行中的牌局", wallet) diff --git a/tests/test_release_063.py b/tests/test_release_063.py index d75bf65..46a40c4 100644 --- a/tests/test_release_063.py +++ b/tests/test_release_063.py @@ -215,9 +215,9 @@ class Release063GameTests(unittest.TestCase): settled = self.command("/21点 要牌", sender="Bob") self.assertIn("这一桌每个人的变化", 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("下注 100,返还 0,净变化 -100;金币 900", settled) + self.assertIn("下注 100,返还 0,净变化 -100;摩拉 900", settled) self.assertEqual(self.state()["balance"], 1200) self.assertEqual(self.state("Bob")["balance"], 900) self.assertIn("alice #1:21点 你赢了", self.command("/游戏记录")) @@ -225,6 +225,11 @@ class Release063GameTests(unittest.TestCase): "bob #1:21点 你输了", 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): with mock.patch.object( @@ -242,12 +247,12 @@ class Release063GameTests(unittest.TestCase): self.assertIn("alice #1:抽奖 未中奖", history) 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") - self.assertIn("bob 还没有金币账户", missing) + self.assertIn("bob 还没有摩拉账户", missing) 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") replay = self.command( "/转账 Bob 250", @@ -256,40 +261,56 @@ class Release063GameTests(unittest.TestCase): ) 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("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): - self.assertIn("金币 1000", self.command("/金币")) + self.assertIn("摩拉 1000", self.command("/金币")) self.assertIn("转账金额必须为 1~100000", self.command("/转账 Bob 0")) self.assertIn("不能给自己转账", self.command("/转账 a1 100")) - self.assertIn("金币 1000", self.command("/金币", sender="Bob")) - self.assertIn("金币不足", self.command("/transfer 1001 Bob")) + self.assertIn("摩拉 1000", self.command("/金币", sender="Bob")) + self.assertIn("摩拉不足", self.command("/transfer 1001 Bob")) self.assertEqual(self.state()["balance"], 1000) self.assertEqual(self.state("Bob")["balance"], 1000) 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") replay = self.command("/贷款 300", event_id="loan-1") self.assertEqual(replay, first) - self.assertIn("贷款 300 金币已到账", first) + self.assertIn("贷款 300 摩拉已到账", first) self.assertEqual(self.state()["balance"], 1300) self.assertEqual(self.state()["loan"]["principal"], 300) self.assertIn("未还贷款 300", self.command("/金币")) self.assertIn("已有未还贷款 300", self.command("/贷款 100")) partial = self.command("/还款 120") - self.assertIn("已还款 120 金币", partial) + self.assertIn("已还款 120 摩拉", partial) self.assertEqual(self.state()["balance"], 1180) self.assertEqual(self.state()["loan"]["principal"], 180) done = self.command("/还款 全部") - self.assertIn("已还清贷款 180 金币", done) + self.assertIn("已还清贷款 180 摩拉", done) self.assertEqual(self.state()["balance"], 1000) 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): self.assertIn("贷款金额必须为 1~500", self.command("/贷款 0")) @@ -297,20 +318,20 @@ class Release063GameTests(unittest.TestCase): self.assertIn("当前没有未还贷款", self.command("/还款 全部")) def test_coin_ranking_lists_accounts_by_balance(self): - self.assertIn("金币 1000", self.command("/金币")) - self.assertIn("金币 1000", self.command("/金币", sender="Bob")) - self.assertIn("金币 1000", self.command("/金币", sender="Carol")) + self.assertIn("摩拉 1000", self.command("/金币")) + self.assertIn("摩拉 1000", self.command("/金币", sender="Bob")) + self.assertIn("摩拉 1000", self.command("/金币", sender="Carol")) self.command("/转账 Bob 250") self.command("/贷款 100", sender="Carol") ranking = self.command("/大富翁榜") self.assertIn("大富翁榜:", ranking) - self.assertIn("1. bob 1250 金币", ranking) - self.assertIn("2. carol 1100 金币(未还贷款 100)", ranking) - self.assertIn("3. alice 750 金币", ranking) + self.assertIn("1. bob 1250 摩拉", ranking) + self.assertIn("2. carol 1100 摩拉(未还贷款 100)", ranking) + self.assertIn("3. alice 750 摩拉", 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): with mock.patch.object( @@ -326,14 +347,14 @@ class Release063GameTests(unittest.TestCase): ): self.command("/抽奖10", sender="Bob") - stats = self.command("/Taffy金币") + stats = self.command("/Taffy摩拉") self.assertIn("玩家总下注 20,总返还 50", stats) - self.assertIn("Taffy累计倒贴 30 金币", stats) + self.assertIn("Taffy累计倒贴 30 摩拉", stats) self.assertIn("最大上贡者排行(含负数):", stats) self.assertIn("alice:上贡 10", 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): root = Path(self.temp.name) diff --git a/tests/test_wechat_adaptive_recorder.py b/tests/test_wechat_adaptive_recorder.py index e047f44..27113a2 100644 --- a/tests/test_wechat_adaptive_recorder.py +++ b/tests/test_wechat_adaptive_recorder.py @@ -423,6 +423,31 @@ class LayoutScalingTests(unittest.TestCase): fast_ack_sender.assert_called_once() 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): legacy = mock.Mock(return_value="legacy") with mock.patch.object( diff --git a/wechat_adaptive_recorder.py b/wechat_adaptive_recorder.py index 071776d..0475746 100755 --- a/wechat_adaptive_recorder.py +++ b/wechat_adaptive_recorder.py @@ -761,6 +761,18 @@ def edge_router_reply( outcome="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( "edge router fallback event_hash=%s status=%s", hashlib.sha256(