diff --git a/CHANGELOG.md b/CHANGELOG.md index 729385a..1836c83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## beta 1.0.5.1016 - 2026-10-16 16:30:44 + +- 新增原石系统:成就可获得原石,并开放祈愿、扫码加权、Taffy发话和原石榜。 +- Add primogem account state, primogem movement history, wish records, scan + boost coupons, Taffy speech credits, scheduled speech delivery, and + primogem-source ranking without a SQLite schema migration. +- Award primogems from blackjack natural 21, blackjack 20-to-21 hit challenges, + Taffy blackjack win streaks, and personal consecutive lottery high-hit pairs. +- Extend post-release settlement so this version grants each existing account + both 200 Mora and 1 primogem, and show `/version` update time down to seconds. + ## beta 1.0.4.1016 - 2026-10-16 - 新增版本更新补偿流程:每次生产更新后向所有既有账户发放 200 摩拉,并在目标群发布更新公告。 diff --git a/README.md b/README.md index 590e727..ee4be98 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC 观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件, 并通过带视觉确认的 GUI 发送链路回复。 -当前版本:`beta 1.0.4.1016`,更新日期:2026-10-16。 +当前版本:`beta 1.0.5.1016`,更新时间:2026-10-16 16:30:44。 1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、 Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major @@ -89,6 +89,8 @@ PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh /彩票 开始|报名|当前状态|开奖|取消(/lottery) /21点 开始 [1~500];开局后 /21点 要牌|停牌|状态|结束 /21点 多人 启动;/21点 加入;/21点 多人 开始;/21点 放200 +/摩拉(兼 /原石);/原石祈愿;/原石榜;/扫码加权 人名;/Taffy发话 内容 +/游戏记录 [页码](/gamehistory) /游戏记录 [页码](/gamehistory) /游戏帮助(/games) /自走棋开始|报名|开赛|状态|档案 @@ -190,7 +192,8 @@ sudo ./deploy/scripts/deploy-jwebot-production.sh /path/to/JWeBot jwebot 部署必须通过微信窗口就绪、队列清空、Edge 健康、SQLite 完整性和自适应 录制服务重启稳定性检查。切换成功后,部署器按版本幂等地向每个既有账户 -发放 200 摩拉、写入账户流水,并使用当前 changelog 的第一条摘要发布群公告。 +发放 200 摩拉、写入账户流水,并使用当前 changelog 的第一条摘要发布群公告; +本次 `beta 1.0.5.1016` 额外发放 1 原石。 旧 OCR 服务保留安装,但保持禁用。 ### 仓库结构 @@ -265,10 +268,10 @@ visually confirmed GUI bridge. Current version: ```text -beta 1.0.4.1016 +beta 1.0.5.1016 ``` -Updated: 2026-10-16. +Updated: 2026-10-16 16:30:44. The 1.0 Beta baseline consolidates the stable local command set, person and context indexes, Steam presence/playtime queries, virtual coin games, @@ -457,6 +460,7 @@ SQLite integrity, and adaptive restart-stability gates pass. Legacy OCR units remain installed but disabled. After cutover, version-idempotent release settlement grants 200 Mora to every existing account, records the movement, and sends the first changelog bullet as the guarded group announcement. +For `beta 1.0.5.1016`, the settlement also grants 1 primogem once. ## Repository Map diff --git a/VERSION b/VERSION index 35ee1fd..ccd83fa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -beta 1.0.4.1016 +beta 1.0.5.1016 diff --git a/docs/command-catalog.md b/docs/command-catalog.md index 548d8c4..4c885ff 100644 --- a/docs/command-catalog.md +++ b/docs/command-catalog.md @@ -1,6 +1,6 @@ # JWeBot Command Catalog -Current baseline: `beta 1.0.1.1016`. +Current baseline: `beta 1.0.5.1016`. This catalog records the reviewed commands shipped with the built-in local extensions. Runtime ownership is frozen at startup from the generated @@ -12,7 +12,7 @@ descriptor inventory; plugin code delegates to reviewed service profiles. |---|---|---| | `/功能列表` | `/help` | Show the command list. | | `/状态` | `/status` | Show local runtime status. | -| `/version` | - | Show installed package versions plus the latest changelog date and summary. | +| `/version` | `/版本` | Show installed package versions plus the latest changelog time and summary. | | `/debug` | - | Show privacy-safe telemetry diagnostics. | | `/余额` | - | Show configured direct-provider balance status. | @@ -41,11 +41,16 @@ descriptor inventory; plugin code delegates to reviewed service profiles. | Command | Alias | Purpose | |---|---|---| -| `/摩拉` | `/金币`, `/wallet` | Initialize or show the current person's Mora account. | +| `/摩拉` | `/金币`, `/原石`, `/wallet` | Initialize or show the current person's Mora and primogem 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. | +| `/原石祈愿` | `/祈愿`, `/wish` | Spend 1 primogem to draw 20-10000 Mora with expected value 3600 Mora, with a chance to gain a scan-boost ticket. | +| `/原石榜 [人数]` | `/primogem榜` | Show the group primogem leaderboard with source details. | +| `/扫码加权 人名` | `/扫码概率 人名` | Spend a scan-boost ticket or 1 primogem to raise that person's hourly scan weight. | +| `/Taffy发话 内容` | `/taffysay content` | Spend Taffy speech credit; 1 primogem converts to 2 credits when needed. | +| `/Taffy发话 定时 HH:MM 内容` | - | Schedule a Taffy speech through the guarded clock delivery queue. | | `/抽奖 [金额]` | `/draw` | Play a single-player prize draw. | | `/彩票 开始` | `/lottery start` | Open a multiplayer lottery round. | | `/彩票 报名 数字 金额` | `/lottery join` | Join or update a lottery entry. | diff --git a/extensions/jwebot/game-history/README.md b/extensions/jwebot/game-history/README.md new file mode 100644 index 0000000..f1d2573 --- /dev/null +++ b/extensions/jwebot/game-history/README.md @@ -0,0 +1,19 @@ +# game-history + +Author: `jwebot` + +游戏记录 + +Help: + +- `/游戏记录 /个人记录 /gamehistory` + +Registered commands: + +- `/游戏记录` +- `/个人记录` +- `/gamehistory` + +The adapter has no direct Ledger, GUI, environment, shell, or network access. +It delegates to the reviewed `game-history` Kernel service profile +and returns a structured `ReplyPlan`. diff --git a/extensions/jwebot/game-history/plugin.toml b/extensions/jwebot/game-history/plugin.toml new file mode 100644 index 0000000..e4b8114 --- /dev/null +++ b/extensions/jwebot/game-history/plugin.toml @@ -0,0 +1,14 @@ +id = "game-history" +author = "jwebot" +description = "游戏记录" +help = ["/游戏记录 /个人记录 /gamehistory"] +version = "1.0.0" +api_version = "1" +entrypoint = "game_history:plugin" +type = "command" + +[capabilities] +commands = ["/游戏记录", "/个人记录", "/gamehistory"] +storage = false +network_profiles = [] +service_profiles = ["game-history"] diff --git a/extensions/jwebot/game-history/src/game_history.py b/extensions/jwebot/game-history/src/game_history.py new file mode 100644 index 0000000..9eab42c --- /dev/null +++ b/extensions/jwebot/game-history/src/game_history.py @@ -0,0 +1,45 @@ +"""Generated reviewed adapter for game-history.""" + +from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext + + +PROFILE = 'game-history' +AUTHOR = 'jwebot' +DESCRIPTION = '游戏记录' +HELP_ENTRIES = ('/游戏记录 /个人记录 /gamehistory',) +COMMANDS = ('/游戏记录', '/个人记录', '/gamehistory') + + +def _handler(context: SingleGroupContext, command: str): + def handle(event: Event, arguments: str) -> ExtensionResult: + response = context.services.call( + PROFILE, + operation="execute", + payload={ + "command": command, + "arguments": arguments, + "event": event.to_mapping(), + }, + ) + texts = response.get("texts") + if isinstance(texts, list) and texts and all( + isinstance(item, str) and item.strip() for item in texts + ): + return ExtensionResult( + reply=ReplyPlan.multi_text(texts), + metadata={"local_extension": PROFILE, "author": AUTHOR}, + ) + text = response.get("text") + if not isinstance(text, str) or not text.strip(): + raise ValueError("local service returned invalid text") + return ExtensionResult( + reply=ReplyPlan.text(text), + metadata={"local_extension": PROFILE, "author": AUTHOR}, + ) + + return handle + + +def plugin(context: SingleGroupContext) -> None: + for command in COMMANDS: + context.commands.register(command, _handler(context, command)) diff --git a/extensions/jwebot/game-history/tests/test_plugin.py b/extensions/jwebot/game-history/tests/test_plugin.py new file mode 100644 index 0000000..856fd07 --- /dev/null +++ b/extensions/jwebot/game-history/tests/test_plugin.py @@ -0,0 +1,29 @@ +import sys +import unittest +from pathlib import Path +from unittest import mock + + +SOURCE = Path(__file__).resolve().parents[1] / "src" +sys.path.insert(0, str(SOURCE)) + +from game_history import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin + + +class PluginTests(unittest.TestCase): + def test_registers_declared_commands_and_author(self): + context = mock.Mock() + plugin(context) + registered = [ + call.args[0] + for call in context.commands.register.call_args_list + ] + self.assertEqual(AUTHOR, 'jwebot') + self.assertEqual(DESCRIPTION, '游戏记录') + self.assertEqual(HELP_ENTRIES, ('/游戏记录 /个人记录 /gamehistory',)) + self.assertIn('/游戏记录', registered) + self.assertEqual(len(registered), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/extensions/jwebot/primogem-economy/README.md b/extensions/jwebot/primogem-economy/README.md new file mode 100644 index 0000000..b0d7083 --- /dev/null +++ b/extensions/jwebot/primogem-economy/README.md @@ -0,0 +1,25 @@ +# primogem-economy + +Author: `jwebot` + +原石系统 + +Help: + +- `/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容` + +Registered commands: + +- `/原石祈愿` +- `/祈愿` +- `/wish` +- `/原石榜` +- `/primogem榜` +- `/扫码加权` +- `/扫码概率` +- `/Taffy发话` +- `/taffysay` + +The adapter has no direct Ledger, GUI, environment, shell, or network access. +It delegates to the reviewed `primogem-economy` Kernel service profile +and returns a structured `ReplyPlan`. diff --git a/extensions/jwebot/primogem-economy/plugin.toml b/extensions/jwebot/primogem-economy/plugin.toml new file mode 100644 index 0000000..5ff6826 --- /dev/null +++ b/extensions/jwebot/primogem-economy/plugin.toml @@ -0,0 +1,14 @@ +id = "primogem-economy" +author = "jwebot" +description = "原石系统" +help = ["/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容"] +version = "1.0.0" +api_version = "1" +entrypoint = "primogem_economy:plugin" +type = "command" + +[capabilities] +commands = ["/原石祈愿", "/祈愿", "/wish", "/原石榜", "/primogem榜", "/扫码加权", "/扫码概率", "/Taffy发话", "/taffysay"] +storage = false +network_profiles = [] +service_profiles = ["primogem-economy"] diff --git a/extensions/jwebot/primogem-economy/src/primogem_economy.py b/extensions/jwebot/primogem-economy/src/primogem_economy.py new file mode 100644 index 0000000..30a6919 --- /dev/null +++ b/extensions/jwebot/primogem-economy/src/primogem_economy.py @@ -0,0 +1,45 @@ +"""Generated reviewed adapter for primogem-economy.""" + +from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext + + +PROFILE = 'primogem-economy' +AUTHOR = 'jwebot' +DESCRIPTION = '原石系统' +HELP_ENTRIES = ('/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容',) +COMMANDS = ('/原石祈愿', '/祈愿', '/wish', '/原石榜', '/primogem榜', '/扫码加权', '/扫码概率', '/Taffy发话', '/taffysay') + + +def _handler(context: SingleGroupContext, command: str): + def handle(event: Event, arguments: str) -> ExtensionResult: + response = context.services.call( + PROFILE, + operation="execute", + payload={ + "command": command, + "arguments": arguments, + "event": event.to_mapping(), + }, + ) + texts = response.get("texts") + if isinstance(texts, list) and texts and all( + isinstance(item, str) and item.strip() for item in texts + ): + return ExtensionResult( + reply=ReplyPlan.multi_text(texts), + metadata={"local_extension": PROFILE, "author": AUTHOR}, + ) + text = response.get("text") + if not isinstance(text, str) or not text.strip(): + raise ValueError("local service returned invalid text") + return ExtensionResult( + reply=ReplyPlan.text(text), + metadata={"local_extension": PROFILE, "author": AUTHOR}, + ) + + return handle + + +def plugin(context: SingleGroupContext) -> None: + for command in COMMANDS: + context.commands.register(command, _handler(context, command)) diff --git a/extensions/jwebot/primogem-economy/tests/test_plugin.py b/extensions/jwebot/primogem-economy/tests/test_plugin.py new file mode 100644 index 0000000..66fc1ef --- /dev/null +++ b/extensions/jwebot/primogem-economy/tests/test_plugin.py @@ -0,0 +1,29 @@ +import sys +import unittest +from pathlib import Path +from unittest import mock + + +SOURCE = Path(__file__).resolve().parents[1] / "src" +sys.path.insert(0, str(SOURCE)) + +from primogem_economy import AUTHOR, DESCRIPTION, HELP_ENTRIES, plugin + + +class PluginTests(unittest.TestCase): + def test_registers_declared_commands_and_author(self): + context = mock.Mock() + plugin(context) + registered = [ + call.args[0] + for call in context.commands.register.call_args_list + ] + self.assertEqual(AUTHOR, 'jwebot') + self.assertEqual(DESCRIPTION, '原石系统') + self.assertEqual(HELP_ENTRIES, ('/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容',)) + self.assertIn('/原石祈愿', registered) + self.assertEqual(len(registered), 9) + + +if __name__ == "__main__": + unittest.main() diff --git a/extensions/jwebot/single-player-games/README.md b/extensions/jwebot/single-player-games/README.md index d64073b..b2a6528 100644 --- a/extensions/jwebot/single-player-games/README.md +++ b/extensions/jwebot/single-player-games/README.md @@ -6,17 +6,14 @@ Author: `jwebot` Help: -- `/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory` -- `/抽奖 [金额];/draw` -- `/彩票 开始|报名|状态|开奖|取消;/lottery` -- `/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200` -- `/游戏帮助;/games` +- `/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助` Registered commands: - `/摩拉` - `/金币` - `/wallet` +- `/原石` - `/抽奖` - `/draw` - `/彩票` @@ -25,9 +22,6 @@ Registered commands: - `/21点` - `/二十一点` - `/blackjack` -- `/游戏记录` -- `/个人记录` -- `/gamehistory` - `/游戏帮助` - `/games` diff --git a/extensions/jwebot/single-player-games/plugin.toml b/extensions/jwebot/single-player-games/plugin.toml index 7285775..4c830ee 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"] +help = ["/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助"] 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", "/游戏帮助", "/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 b58aa07..06704c2 100644 --- a/extensions/jwebot/single-player-games/src/single_player_games.py +++ b/extensions/jwebot/single-player-games/src/single_player_games.py @@ -6,8 +6,8 @@ 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') +HELP_ENTRIES = ('/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助',) +COMMANDS = ('/摩拉', '/金币', '/wallet', '/原石', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/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 29829aa..981e890 100644 --- a/extensions/jwebot/single-player-games/tests/test_plugin.py +++ b/extensions/jwebot/single-player-games/tests/test_plugin.py @@ -20,9 +20,9 @@ class PluginTests(unittest.TestCase): ] self.assertEqual(AUTHOR, 'jwebot') self.assertEqual(DESCRIPTION, '摩拉游戏') - self.assertEqual(HELP_ENTRIES, ('/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory', '/抽奖 [金额];/draw', '/彩票 开始|报名|状态|开奖|取消;/lottery', '/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200', '/游戏帮助;/games')) + self.assertEqual(HELP_ENTRIES, ('/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助',)) self.assertIn('/摩拉', registered) - self.assertEqual(len(registered), 16) + self.assertEqual(len(registered), 14) if __name__ == "__main__": diff --git a/extensions/official/jwebot-version/README.md b/extensions/official/jwebot-version/README.md index 06e9bd9..131945f 100644 --- a/extensions/official/jwebot-version/README.md +++ b/extensions/official/jwebot-version/README.md @@ -6,10 +6,11 @@ JWeBot 版本 Help: -- `/version` +- `/version /版本` Registered commands: +- `/版本` - `/version` The adapter has no direct Ledger, GUI, environment, shell, or network access. diff --git a/extensions/official/jwebot-version/plugin.toml b/extensions/official/jwebot-version/plugin.toml index 9d766d4..3e12543 100644 --- a/extensions/official/jwebot-version/plugin.toml +++ b/extensions/official/jwebot-version/plugin.toml @@ -1,14 +1,14 @@ id = "jwebot-version" author = "official" description = "JWeBot 版本" -help = ["/version"] +help = ["/version /版本"] version = "1.0.0" api_version = "1" entrypoint = "jwebot_version:plugin" type = "command" [capabilities] -commands = ["/version"] +commands = ["/版本", "/version"] storage = false network_profiles = [] service_profiles = ["jwebot-version"] diff --git a/extensions/official/jwebot-version/src/jwebot_version.py b/extensions/official/jwebot-version/src/jwebot_version.py index 871eec1..265c0c4 100644 --- a/extensions/official/jwebot-version/src/jwebot_version.py +++ b/extensions/official/jwebot-version/src/jwebot_version.py @@ -6,8 +6,8 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'jwebot-version' AUTHOR = 'official' DESCRIPTION = 'JWeBot 版本' -HELP_ENTRIES = ('/version',) -COMMANDS = ('/version',) +HELP_ENTRIES = ('/version /版本',) +COMMANDS = ('/版本', '/version') def _handler(context: SingleGroupContext, command: str): diff --git a/extensions/official/jwebot-version/tests/test_plugin.py b/extensions/official/jwebot-version/tests/test_plugin.py index 2f82e02..3c8bc7a 100644 --- a/extensions/official/jwebot-version/tests/test_plugin.py +++ b/extensions/official/jwebot-version/tests/test_plugin.py @@ -20,9 +20,9 @@ class PluginTests(unittest.TestCase): ] self.assertEqual(AUTHOR, 'official') self.assertEqual(DESCRIPTION, 'JWeBot 版本') - self.assertEqual(HELP_ENTRIES, ('/version',)) - self.assertIn('/version', registered) - self.assertEqual(len(registered), 1) + self.assertEqual(HELP_ENTRIES, ('/version /版本',)) + self.assertIn('/版本', registered) + self.assertEqual(len(registered), 2) if __name__ == "__main__": diff --git a/extensions/official/person-directory/README.md b/extensions/official/person-directory/README.md index da453f7..ae8ce00 100644 --- a/extensions/official/person-directory/README.md +++ b/extensions/official/person-directory/README.md @@ -2,7 +2,7 @@ Author: `official` -查询当前群已配置人物、基本属性和别名 +人物索引 Help: diff --git a/extensions/official/person-directory/plugin.toml b/extensions/official/person-directory/plugin.toml index 37c24e1..c30f06a 100644 --- a/extensions/official/person-directory/plugin.toml +++ b/extensions/official/person-directory/plugin.toml @@ -1,6 +1,6 @@ id = "person-directory" author = "official" -description = "查询当前群已配置人物、基本属性和别名" +description = "人物索引" help = ["/人物列表 [页码] /people;/人物 [名称或别名] /person"] version = "1.0.0" api_version = "1" diff --git a/extensions/official/person-directory/src/person_directory.py b/extensions/official/person-directory/src/person_directory.py index 9176253..05607ed 100644 --- a/extensions/official/person-directory/src/person_directory.py +++ b/extensions/official/person-directory/src/person_directory.py @@ -5,7 +5,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'person-directory' AUTHOR = 'official' -DESCRIPTION = '查询当前群已配置人物、基本属性和别名' +DESCRIPTION = '人物索引' HELP_ENTRIES = ('/人物列表 [页码] /people;/人物 [名称或别名] /person',) COMMANDS = ('/人物列表', '/别名列表', '/people', '/人物', '/个人属性', '/person') diff --git a/extensions/official/person-directory/tests/test_plugin.py b/extensions/official/person-directory/tests/test_plugin.py index 68a64aa..3829f62 100644 --- a/extensions/official/person-directory/tests/test_plugin.py +++ b/extensions/official/person-directory/tests/test_plugin.py @@ -19,7 +19,7 @@ class PluginTests(unittest.TestCase): for call in context.commands.register.call_args_list ] self.assertEqual(AUTHOR, 'official') - self.assertEqual(DESCRIPTION, '查询当前群已配置人物、基本属性和别名') + self.assertEqual(DESCRIPTION, '人物索引') self.assertEqual(HELP_ENTRIES, ('/人物列表 [页码] /people;/人物 [名称或别名] /person',)) self.assertIn('/人物列表', registered) self.assertEqual(len(registered), 6) diff --git a/jwebot_release_update.py b/jwebot_release_update.py index 3a5db3a..1903d87 100644 --- a/jwebot_release_update.py +++ b/jwebot_release_update.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Apply the mandatory post-release Mora grant and group announcement.""" +"""Apply the mandatory post-release grant and group announcement.""" from __future__ import annotations @@ -26,11 +26,15 @@ sys.path[:0] = [ from jwebot_official_extensions import __version__ as OFFICIAL_VERSION # noqa: E402 from jwebot_official_extensions.player_games import ( # noqa: E402 append_coin_event, + append_primogem_event, normalize_player_account, ) COMPENSATION_AMOUNT = 200 +PRIMOGEM_COMPENSATION_RELEASES = { + "beta 1.0.5.1016": 1, +} MAX_BALANCE = 1_000_000_000 MAX_SUMMARY_CHARS = 300 MAX_ANNOUNCEMENT_CHARS = 900 @@ -39,7 +43,7 @@ RELEASE_PATTERN = re.compile( ) CHANGELOG_HEADING = re.compile( r"^##\s+(?Pbeta \d+\.\d+\.\d+\.\d{4})" - r"(?:\s+-\s+\d{4}-\d{2}-\d{2})?\s*$" + r"(?:\s+-\s+\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}:\d{2})?)?\s*$" ) @@ -119,11 +123,13 @@ def read_changelog_summary(path: Path, release: str) -> str: def build_announcement(release: str, summary: str) -> str: + primogems = primogem_compensation_amount(release) + extra = f",原石 x{primogems}" if primogems else "" announcement = ( "亲爱的群友:\n" f"{release} 更新现已完成。\n" f"更新内容:{summary}\n" - f"更新补偿:摩拉 x{COMPENSATION_AMOUNT}\n" + f"更新补偿:摩拉 x{COMPENSATION_AMOUNT}{extra}\n" "补偿范围:截至本公告发布前已创建摩拉账户的玩家\n" "发放方式:补偿已直接到账,无需领取\n" "感谢大家对 Taffy 的支持与理解,祝大家游戏愉快!" @@ -137,6 +143,10 @@ def compensation_key(release: str) -> str: return f"release_compensation:{release}" +def primogem_compensation_amount(release: str) -> int: + return int(PRIMOGEM_COMPENSATION_RELEASES.get(release, 0)) + + def announcement_key(release: str, group_fingerprint: str) -> str: group_hash = hashlib.sha256(group_fingerprint.encode()).hexdigest() return f"release_announcement:{release}:{group_hash}" @@ -147,6 +157,7 @@ def release_slug(release: str) -> str: def checked_marker(raw: str, release: str) -> dict[str, Any]: + expected_primogems = primogem_compensation_amount(release) try: marker = json.loads(raw) except json.JSONDecodeError as error: @@ -155,6 +166,8 @@ def checked_marker(raw: str, release: str) -> dict[str, Any]: not isinstance(marker, dict) or marker.get("release") != release or marker.get("amount") != COMPENSATION_AMOUNT + or marker.get("mora_amount") not in {None, COMPENSATION_AMOUNT} + or marker.get("primogem_amount") not in {None, expected_primogems} or not isinstance(marker.get("group_fingerprints"), list) ): raise ReleaseUpdateError("release compensation marker does not match") @@ -234,8 +247,11 @@ def apply_compensation( ).fetchall() groups = sorted({str(row["group_fingerprint"]) for row in rows}) old_balance_sum = 0 + old_primogem_sum = 0 + primogem_amount = primogem_compensation_amount(release) timestamp = isoformat_utc(applied_at) event_reason = f"{release} 版本更新补偿" + primogem_reason = f"{release} 版本更新补偿" for row in rows: try: state = json.loads(str(row["state_json"])) @@ -251,9 +267,20 @@ def apply_compensation( ): raise ReleaseUpdateError("Mora account balance is invalid") old_balance_sum += balance + old_primogem_sum += int(state["primogems"]) state["balance"] = balance + COMPENSATION_AMOUNT + state["primogems"] += primogem_amount append_coin_event(state, COMPENSATION_AMOUNT, event_reason) state["coin_events"][-1]["created_at"] = applied_at.timestamp() + if primogem_amount: + append_primogem_event( + state, + primogem_amount, + primogem_reason, + ) + state["primogem_events"][-1]["created_at"] = ( + applied_at.timestamp() + ) connection.execute( """ UPDATE player_accounts @@ -270,12 +297,18 @@ def apply_compensation( marker = { "release": release, "amount": COMPENSATION_AMOUNT, + "mora_amount": COMPENSATION_AMOUNT, + "primogem_amount": primogem_amount, "accounts": len(rows), "group_fingerprints": groups, "old_balance_sum": old_balance_sum, "new_balance_sum": ( old_balance_sum + COMPENSATION_AMOUNT * len(rows) ), + "old_primogem_sum": old_primogem_sum, + "new_primogem_sum": ( + old_primogem_sum + primogem_amount * len(rows) + ), "applied_at": timestamp, "backup": backup_path.name, } @@ -523,6 +556,7 @@ def run_release_update( "release": release, "compensation": compensation_state, "amount_each": COMPENSATION_AMOUNT, + "primogems_each": primogem_compensation_amount(release), "accounts": int(marker["accounts"]), "groups": len(groups), "announcements_delivered": announcement_result["delivered"], diff --git a/packages/jwebot-config/pyproject.toml b/packages/jwebot-config/pyproject.toml index 4025680..e08f2af 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.4b1+1016" +version = "1.0.5b1+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 cbef4cb..2ee22ba 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.4b1+1016" +__version__ = "1.0.5b1+1016" diff --git a/packages/jwebot-extension-host/pyproject.toml b/packages/jwebot-extension-host/pyproject.toml index 04e7636..0dbba3b 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.4b1+1016" +version = "1.0.5b1+1016" description = "Single-release, single-invocation process host for JWeBot extensions" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.0.4b1+1016", - "jwebot-kernel==1.0.4b1+1016", + "jwebot-sdk==1.0.5b1+1016", + "jwebot-kernel==1.0.5b1+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 e51be5e..102b72c 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.4b1+1016" +__version__ = "1.0.5b1+1016" diff --git a/packages/jwebot-kernel/pyproject.toml b/packages/jwebot-kernel/pyproject.toml index f7183bb..74950c6 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.4b1+1016" +version = "1.0.5b1+1016" description = "Stable routing and extension authority for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-config==1.0.4b1+1016", - "jwebot-sdk==1.0.4b1+1016", - "jwebot-official-extensions==1.0.4b1+1016", + "jwebot-config==1.0.5b1+1016", + "jwebot-sdk==1.0.5b1+1016", + "jwebot-official-extensions==1.0.5b1+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 e22c24e..9bf01e0 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.4b1+1016" +__version__ = "1.0.5b1+1016" diff --git a/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py b/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py index 9d87ef3..702aae1 100644 --- a/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py +++ b/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py @@ -74,9 +74,12 @@ from jwebot_official_extensions.player_games import ( append_coin_event, append_game_coin_event, append_game_record, + blackjack_achievement_lines, hand_value, + lottery_achievement_lines, next_game_index, normalize_player_account, + spend_primogems, ) from jwebot_sdk import Event, PlanKind @@ -2847,13 +2850,54 @@ class Ledger: timestamp, ), ) + if cursor.rowcount == 0 and kind == "taffy_speech": + cursor = self.connection.execute( + """ + UPDATE clock_broadcasts + SET state = 'pending', updated_at = ? + WHERE group_fingerprint = ? AND slot_key = ? + AND kind = 'taffy_speech' AND state = 'scheduled' + """, + ( + timestamp, + group_fingerprint, + slot_key, + ), + ) return cursor.rowcount == 1 + def due_scheduled_taffy_speech( + self, + group_fingerprint: str, + now: datetime, + ) -> dict[str, Any] | None: + group = bounded_text(group_fingerprint, limit=256) + if not group: + return None + local_now = now.astimezone(BEIJING_TIMEZONE) + due_prefix = local_now.strftime("taffy-speech:%Y-%m-%dT%H:%M") + with self.lock: + row = self.connection.execute( + """ + SELECT group_fingerprint, slot_key, local_date, kind, message + FROM clock_broadcasts + WHERE group_fingerprint = ? + AND kind = 'taffy_speech' + AND state = 'scheduled' + AND slot_key <= ? + ORDER BY slot_key + LIMIT 1 + """, + (group, due_prefix + ":~"), + ).fetchone() + return dict(row) if row else None + def complete_clock_broadcast( self, group_fingerprint: str, slot_key: str, ) -> bool: + consume_boost_key = "" with self.lock, self.connection: row = self.connection.execute( """ @@ -2894,6 +2938,9 @@ class Ledger: utc_now(), ), ) + consume_boost_key = person_key + if consume_boost_key: + self.consume_scan_boost(group_fingerprint, consume_boost_key) return True def fail_clock_broadcast( @@ -3609,6 +3656,10 @@ class Ledger: stake=stake, payout=payout, ) + achievements = lottery_achievement_lines( + account, + matches=matches, + ) self._save_player_account( group, key, account, timestamp ) @@ -3634,6 +3685,7 @@ class Ledger: "matches": matches, "payout": payout, "balance": int(account["balance"]), + "achievements": achievements, } ) settled.append(result) @@ -3895,6 +3947,18 @@ class Ledger: stake=bet, payout=payout, ) + achievements = blackjack_achievement_lines( + account, + outcome=outcome, + natural_player_21=( + len(player["hand"]) == 2 and player_value == 21 + ), + extreme_21=( + len(player["hand"]) > 2 + and hand_value(player["hand"][:-1]) == 20 + and player_value == 21 + ), + ) self._save_player_account(group, key, account, timestamp) self.connection.execute( """ @@ -3922,6 +3986,7 @@ class Ledger: "balance": int(account["balance"]), "player_value": player_value, "dealer_value": dealer_value, + "achievements": achievements, } ) settled.append(result) @@ -4927,6 +4992,216 @@ class Ledger: ) return records[: max(1, min(int(limit), 20))] + def player_primogem_ranking( + self, + group: str, + *, + limit: int = 10, + ) -> list[dict[str, Any]]: + group = bounded_text(group, limit=256) + self.seed_person_aliases(group) + with self.lock: + account_rows = [ + dict(row) + for row in self.connection.execute( + """ + SELECT person_key, state_json, updated_at + FROM player_accounts + WHERE group_fingerprint = ? + """, + (group,), + ).fetchall() + ] + alias_rows = [ + dict(row) + for row in self.connection.execute( + """ + SELECT DISTINCT canonical_name + FROM person_aliases + WHERE group_fingerprint = ? + """, + (group,), + ).fetchall() + ] + profile_rows = [ + dict(row) + for row in self.connection.execute( + """ + SELECT person_key, display_name + FROM person_profiles + WHERE group_fingerprint = ? + """, + (group,), + ).fetchall() + ] + observation_rows = [ + dict(row) + for row in self.connection.execute( + """ + SELECT person_key, display_name + FROM person_observations + WHERE group_fingerprint = ? + ORDER BY observed_at DESC, created_at DESC + """, + (group,), + ).fetchall() + ] + labels: dict[str, str] = {} + for row in alias_rows: + canonical = bounded_text(row["canonical_name"], limit=120) + if canonical: + labels[person_scope_key(group, canonical)] = person_display_label( + canonical + ) + for row in profile_rows: + person_key = str(row["person_key"]) + labels.setdefault( + person_key, + person_display_label(row["display_name"]), + ) + for row in observation_rows: + person_key = str(row["person_key"]) + labels.setdefault( + person_key, + person_display_label(row["display_name"]), + ) + records: list[dict[str, Any]] = [] + for row in account_rows: + person_key = str(row["person_key"]) + try: + account = json.loads(str(row["state_json"])) + except json.JSONDecodeError as error: + raise LocalCommandError("原石账户状态无效") from error + if not isinstance(account, dict): + raise LocalCommandError("原石账户状态无效") + normalize_player_account(account) + events = [ + event + for event in account["primogem_events"] + if int(event.get("delta", 0)) > 0 + ] + source_counts: dict[str, int] = {} + for event in events: + reason = bounded_text(event.get("reason"), limit=40) or "原石来源" + source_counts[reason] = source_counts.get(reason, 0) + int( + event["delta"] + ) + sources = "、".join( + f"{reason}+{amount}" + for reason, amount in sorted( + source_counts.items(), + key=lambda item: (-item[1], item[0]), + )[:4] + ) + if not sources: + sources = "暂无正向来源" + records.append( + { + "display_name": labels.get( + person_key, + f"玩家 {person_key[:8]}", + ), + "primogems": int(account["primogems"]), + "sources": sources, + "updated_at": str(row["updated_at"]), + } + ) + records = [ + record + for record in records + if int(record["primogems"]) > 0 or record["sources"] != "暂无正向来源" + ] + records.sort( + key=lambda item: ( + -int(item["primogems"]), + str(item["display_name"]).casefold(), + ) + ) + return records[: max(1, min(int(limit), 20))] + + def active_scan_boost_targets( + self, + group: str, + ) -> dict[str, dict[str, Any]]: + group = bounded_text(group, limit=256) + boosts: dict[str, dict[str, Any]] = {} + with self.lock: + rows = self.connection.execute( + """ + SELECT state_json + FROM player_accounts + WHERE group_fingerprint = ? + """, + (group,), + ).fetchall() + for row in rows: + try: + account = json.loads(str(row["state_json"])) + except json.JSONDecodeError as error: + raise LocalCommandError("原石账户状态无效") from error + if not isinstance(account, dict): + raise LocalCommandError("原石账户状态无效") + normalize_player_account(account) + for boost in account["scan_boosts"]: + target_key = str(boost["target_person_key"]) + current = boosts.setdefault( + target_key, + { + "person_key": target_key, + "display_name": str(boost["target_display_name"]), + "extra_weight": 0, + }, + ) + current["extra_weight"] += int(boost["weight"]) + return boosts + + def consume_scan_boost( + self, + group: str, + target_person_key: str, + ) -> None: + group = bounded_text(group, limit=256) + target_key = bounded_text(target_person_key, limit=128) + if not group or not target_key: + return + timestamp = utc_now() + with self.lock, self.connection: + rows = self.connection.execute( + """ + SELECT person_key, state_json + FROM player_accounts + WHERE group_fingerprint = ? + """, + (group,), + ).fetchall() + for row in rows: + try: + account = json.loads(str(row["state_json"])) + except json.JSONDecodeError as error: + raise LocalCommandError("原石账户状态无效") from error + if not isinstance(account, dict): + raise LocalCommandError("原石账户状态无效") + normalize_player_account(account) + changed = False + for boost in account["scan_boosts"]: + if str(boost["target_person_key"]) == target_key: + boost["remaining"] = int(boost["remaining"]) - 1 + changed = True + break + if changed: + account["scan_boosts"] = [ + boost + for boost in account["scan_boosts"] + if int(boost["remaining"]) > 0 + ] + self._save_player_account( + group, + str(row["person_key"]), + account, + timestamp, + ) + return + def player_taffy_coin_stats( self, group: str, @@ -5156,6 +5431,97 @@ class Ledger: self.connection.execute("ROLLBACK") raise + def player_taffy_say( + self, + group: str, + sender: str, + event_id: str, + command: str, + content: str, + scheduled_at: float | None, + render: Callable[[dict[str, Any]], str], + ) -> str: + person = self._player_identity(group, sender) + person_key = str(person["person_key"]) + message = bounded_text(f"Taffy:{content}", limit=MAX_REPLY_CHARS) + if not message or message == "Taffy:": + raise LocalCommandError("Taffy发话内容不能为空") + with self.lock: + self.connection.execute("BEGIN IMMEDIATE") + try: + replay = self._player_action_replay( + group, event_id, person_key, command + ) + if replay is not None: + self.connection.execute("COMMIT") + return replay + account = self._load_player_account(group, person_key) + if int(account["taffy_say_credits"]) <= 0: + if int(account["primogems"]) <= 0: + data = {"status": "insufficient"} + else: + spend_primogems(account, 1, "兑换Taffy发话2次") + account["taffy_say_credits"] += 2 + if int(account["taffy_say_credits"]) > 0: + account["taffy_say_credits"] -= 1 + timestamp = utc_now() + if scheduled_at is None: + data = { + "status": "sent", + "content": content, + "primogems": int(account["primogems"]), + "taffy_say_credits": int( + account["taffy_say_credits"] + ), + } + else: + scheduled = datetime.fromtimestamp( + scheduled_at, + BEIJING_TIMEZONE, + ) + scheduled_key = scheduled.strftime("%Y-%m-%dT%H:%M") + slot_key = f"taffy-speech:{scheduled_key}:{event_id}" + self.connection.execute( + """ + INSERT OR IGNORE INTO clock_broadcasts ( + group_fingerprint, slot_key, local_date, kind, + message, scan_person_key, scan_label, state, + created_at, updated_at + ) VALUES (?, ?, ?, 'taffy_speech', ?, '', '', + 'scheduled', ?, ?) + """, + ( + group, + slot_key, + scheduled.date().isoformat(), + message, + timestamp, + timestamp, + ), + ) + data = { + "status": "scheduled", + "content": content, + "scheduled_text": scheduled.strftime( + "%m月%d日 %H:%M" + ), + "primogems": int(account["primogems"]), + "taffy_say_credits": int( + account["taffy_say_credits"] + ), + } + response = render(data) + timestamp = utc_now() + self._save_player_account(group, person_key, account, timestamp) + self._store_player_action( + group, event_id, person_key, command, response, timestamp + ) + self.connection.execute("COMMIT") + return response + except BaseException: + self.connection.execute("ROLLBACK") + raise + def apply_player_action( self, group: str, sender: str, event_id: str, command: str, action: Callable[[dict[str, Any]], str], @@ -5570,7 +5936,18 @@ class HourlyBroadcastEngine: if local_now.minute >= CLOCK_GRACE_MINUTES: return None group = group_fingerprint or self.active_group_fingerprint() - if not group or not self.ledger.is_clock_broadcast_group(group): + if not group: + return None + scheduled = self.ledger.due_scheduled_taffy_speech(group, local_now) + if scheduled is not None: + return ClockBroadcast( + group_fingerprint=str(scheduled["group_fingerprint"]), + slot_key=str(scheduled["slot_key"]), + local_date=str(scheduled["local_date"]), + kind=str(scheduled["kind"]), + message=str(scheduled["message"]), + ) + if not self.ledger.is_clock_broadcast_group(group): return None if local_now.hour == 0: previous_day = (local_now - timedelta(days=1)).date() @@ -5597,14 +5974,33 @@ class HourlyBroadcastEngine: ) local_date = local_now.date().isoformat() - if not CLOCK_SCAN_TARGETS: + weighted_targets: list[dict[str, str]] = [ + { + "person_key": person_scope_key(group, canonical_name), + "label": scan_label, + } + for canonical_name, scan_label in CLOCK_SCAN_TARGETS + ] + boosts = self.ledger.active_scan_boost_targets(group) + for target_key, boost in boosts.items(): + weighted_targets.extend( + [ + { + "person_key": target_key, + "label": str(boost["display_name"]), + } + ] + * max(0, int(boost.get("extra_weight", 0))) + ) + if not weighted_targets: return None target_index = deterministic_int( f"{group}|{local_date}|{local_now.hour:02d}|clock-scan", 0, - len(CLOCK_SCAN_TARGETS) - 1, + len(weighted_targets) - 1, ) - canonical_name, scan_label = CLOCK_SCAN_TARGETS[target_index] + target = weighted_targets[target_index] + scan_label = target["label"] return ClockBroadcast( group_fingerprint=group, slot_key=f"hourly:{local_date}T{local_now.hour:02d}", @@ -5614,7 +6010,7 @@ class HourlyBroadcastEngine: f"现在是北京时间 {local_now.hour}:00," f"{scan_label} 扫码了。" ), - scan_person_key=person_scope_key(group, canonical_name), + scan_person_key=target["person_key"], scan_label=scan_label, ) diff --git a/packages/jwebot-official-extensions/pyproject.toml b/packages/jwebot-official-extensions/pyproject.toml index 53219fe..3104a25 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.4b1+1016" +version = "1.0.5b1+1016" description = "Reviewed local command adapters and broker implementation for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.0.4b1+1016", + "jwebot-sdk==1.0.5b1+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 5e3bc5e..12ff0f4 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.4b1+1016" +__version__ = "1.0.5b1+1016" diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/game_history.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/game_history.py new file mode 100644 index 0000000..9eab42c --- /dev/null +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/game_history.py @@ -0,0 +1,45 @@ +"""Generated reviewed adapter for game-history.""" + +from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext + + +PROFILE = 'game-history' +AUTHOR = 'jwebot' +DESCRIPTION = '游戏记录' +HELP_ENTRIES = ('/游戏记录 /个人记录 /gamehistory',) +COMMANDS = ('/游戏记录', '/个人记录', '/gamehistory') + + +def _handler(context: SingleGroupContext, command: str): + def handle(event: Event, arguments: str) -> ExtensionResult: + response = context.services.call( + PROFILE, + operation="execute", + payload={ + "command": command, + "arguments": arguments, + "event": event.to_mapping(), + }, + ) + texts = response.get("texts") + if isinstance(texts, list) and texts and all( + isinstance(item, str) and item.strip() for item in texts + ): + return ExtensionResult( + reply=ReplyPlan.multi_text(texts), + metadata={"local_extension": PROFILE, "author": AUTHOR}, + ) + text = response.get("text") + if not isinstance(text, str) or not text.strip(): + raise ValueError("local service returned invalid text") + return ExtensionResult( + reply=ReplyPlan.text(text), + metadata={"local_extension": PROFILE, "author": AUTHOR}, + ) + + return handle + + +def plugin(context: SingleGroupContext) -> None: + for command in COMMANDS: + context.commands.register(command, _handler(context, command)) diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/jwebot_version.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/jwebot_version.py index 871eec1..265c0c4 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/jwebot_version.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/jwebot_version.py @@ -6,8 +6,8 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'jwebot-version' AUTHOR = 'official' DESCRIPTION = 'JWeBot 版本' -HELP_ENTRIES = ('/version',) -COMMANDS = ('/version',) +HELP_ENTRIES = ('/version /版本',) +COMMANDS = ('/版本', '/version') def _handler(context: SingleGroupContext, command: str): diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/person_directory.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/person_directory.py index 9176253..05607ed 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/person_directory.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/person_directory.py @@ -5,7 +5,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext PROFILE = 'person-directory' AUTHOR = 'official' -DESCRIPTION = '查询当前群已配置人物、基本属性和别名' +DESCRIPTION = '人物索引' HELP_ENTRIES = ('/人物列表 [页码] /people;/人物 [名称或别名] /person',) COMMANDS = ('/人物列表', '/别名列表', '/people', '/人物', '/个人属性', '/person') diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/primogem_economy.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/primogem_economy.py new file mode 100644 index 0000000..30a6919 --- /dev/null +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/adapters/primogem_economy.py @@ -0,0 +1,45 @@ +"""Generated reviewed adapter for primogem-economy.""" + +from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext + + +PROFILE = 'primogem-economy' +AUTHOR = 'jwebot' +DESCRIPTION = '原石系统' +HELP_ENTRIES = ('/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容',) +COMMANDS = ('/原石祈愿', '/祈愿', '/wish', '/原石榜', '/primogem榜', '/扫码加权', '/扫码概率', '/Taffy发话', '/taffysay') + + +def _handler(context: SingleGroupContext, command: str): + def handle(event: Event, arguments: str) -> ExtensionResult: + response = context.services.call( + PROFILE, + operation="execute", + payload={ + "command": command, + "arguments": arguments, + "event": event.to_mapping(), + }, + ) + texts = response.get("texts") + if isinstance(texts, list) and texts and all( + isinstance(item, str) and item.strip() for item in texts + ): + return ExtensionResult( + reply=ReplyPlan.multi_text(texts), + metadata={"local_extension": PROFILE, "author": AUTHOR}, + ) + text = response.get("text") + if not isinstance(text, str) or not text.strip(): + raise ValueError("local service returned invalid text") + return ExtensionResult( + reply=ReplyPlan.text(text), + metadata={"local_extension": PROFILE, "author": AUTHOR}, + ) + + return handle + + +def plugin(context: SingleGroupContext) -> None: + for command in COMMANDS: + context.commands.register(command, _handler(context, command)) 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 b58aa07..06704c2 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 @@ -6,8 +6,8 @@ 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') +HELP_ENTRIES = ('/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助',) +COMMANDS = ('/摩拉', '/金币', '/wallet', '/原石', '/抽奖', '/draw', '/彩票', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/games') def _handler(context: SingleGroupContext, command: str): 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 a02d09d..1caaea5 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/descriptors.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/descriptors.py @@ -34,6 +34,7 @@ LOCAL_COMMAND_ALIASES = { "历史搜索": "history_search", "状态": "local_status", "status": "server_status", + "版本": "jwebot_version", "version": "jwebot_version", "debug": "telemetry_debug", "余额": "deepseek_balance", @@ -52,6 +53,16 @@ LOCAL_COMMAND_ALIASES = { "Taffy摩拉": "taffy_coins", "Taffy金币": "taffy_coins", "taffycoins": "taffy_coins", + "原石": "wallet", + "原石祈愿": "primogem_wish", + "祈愿": "primogem_wish", + "wish": "primogem_wish", + "原石榜": "primogem_ranking", + "primogem榜": "primogem_ranking", + "扫码加权": "scan_boost", + "扫码概率": "scan_boost", + "Taffy发话": "taffy_say", + "taffysay": "taffy_say", "转账": "transfer", "transfer": "transfer", "贷款": "loan", @@ -158,21 +169,23 @@ LOCAL_EXTENSIONS = ( author=JWEBOT_AUTHOR, description="摩拉游戏", help_entries=( - "/摩拉(兼容 /金币)/wallet;/游戏记录 /gamehistory", - "/抽奖 [金额];/draw", - "/彩票 开始|报名|状态|开奖|取消;/lottery", - "/21点 单人开始|要牌|停牌|状态;多人启动|加入|开始|放200", - "/游戏帮助;/games", + "/摩拉/原石 /抽奖 /彩票 /21点 /游戏帮助", ), command_kinds=( "wallet", "prize_draw", "lottery", "blackjack", - "game_history", "game_rules", ), ), + LocalExtensionDescriptor( + extension_id="game-history", + author=JWEBOT_AUTHOR, + description="游戏记录", + help_entries=("/游戏记录 /个人记录 /gamehistory",), + command_kinds=("game_history",), + ), LocalExtensionDescriptor( extension_id="coin-transfer", author=JWEBOT_AUTHOR, @@ -194,6 +207,20 @@ LOCAL_EXTENSIONS = ( help_entries=("/Taffy摩拉(兼容 /Taffy金币)",), command_kinds=("taffy_coins",), ), + LocalExtensionDescriptor( + extension_id="primogem-economy", + author=JWEBOT_AUTHOR, + description="原石系统", + help_entries=( + "/原石祈愿 /原石榜;/扫码加权 人;/Taffy发话 [定时HH:MM] 内容", + ), + command_kinds=( + "primogem_wish", + "primogem_ranking", + "scan_boost", + "taffy_say", + ), + ), LocalExtensionDescriptor( extension_id="coin-loan", author=JWEBOT_AUTHOR, @@ -213,7 +240,7 @@ LOCAL_EXTENSIONS = ( LocalExtensionDescriptor( extension_id="person-directory", author=OFFICIAL_AUTHOR, - description="查询当前群已配置人物、基本属性和别名", + description="人物索引", help_entries=("/人物列表 [页码] /people;/人物 [名称或别名] /person",), command_kinds=("person_directory", "person_info"), ), @@ -244,7 +271,7 @@ LOCAL_EXTENSIONS = ( extension_id="jwebot-version", author=OFFICIAL_AUTHOR, description="JWeBot 版本", - help_entries=("/version",), + help_entries=("/version /版本",), command_kinds=("jwebot_version",), ), 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 764cb3d..bab8cb9 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 @@ -27,7 +27,9 @@ BEIJING_TIMEZONE = ZoneInfo("Asia/Shanghai") DICE_PATTERN = re.compile(r"^(?:(\d{1,2})[dD](\d{1,3}))?$") RANDOM_NUMBER_PATTERN = re.compile(r"^(-?\d{1,6})\s+(-?\d{1,6})$") MULTI_REPLY_MARKER = "\x1eJWEBOT_MULTI_REPLY_V1\x1e" -CHANGELOG_HEADING = re.compile(r"^##\s+(.+?)(?:\s+-\s+(\d{4}-\d{2}-\d{2}))?\s*$") +CHANGELOG_HEADING = re.compile( + r"^##\s+(.+?)(?:\s+-\s+(\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}:\d{2})?))?\s*$" +) class LocalCommandError(RuntimeError): @@ -166,6 +168,10 @@ class LocalCommandEngine: "blackjack", "game_history", "game_rules", + "primogem_wish", + "primogem_ranking", + "scan_boost", + "taffy_say", }: return self.reply_cleaner(self.players.handle(row, kind, arguments)) if kind == "cs_autochess": @@ -433,6 +439,8 @@ class LocalCommandEngine: continue version = match.group(1).strip() updated_at = (match.group(2) or "").strip() + if re.fullmatch(r"\d{4}-\d{2}-\d{2}", updated_at): + updated_at = f"{updated_at} 00:00:00" bullet_parts: list[str] = [] collecting = False for item in lines[index + 1 :]: 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 4d3f2b8..c1ea893 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 @@ -28,6 +28,26 @@ GAME_HISTORY_LIMIT = 50 GAME_HISTORY_PAGE_SIZE = 5 COIN_EVENT_LIMIT = 50 COIN_EVENT_DISPLAY_LIMIT = 10 +PRIMOGEM_MORA_VALUE = 3600 +PRIMOGEM_EVENT_LIMIT = 50 +PRIMOGEM_EVENT_DISPLAY_LIMIT = 5 +WISH_HISTORY_LIMIT = 30 +SCAN_BOOST_USES = 24 +SCAN_BOOST_WEIGHT = 4 +TAFFY_SAY_CREDITS_PER_PRIMOGEM = 2 +TAFFY_SAY_MAX_CHARS = 160 +WISH_MORA_TABLE = ( + (20, 5), + (100, 12), + (500, 11), + (1000, 16), + (1800, 8), + (3600, 13), + (6000, 12), + (8000, 13), + (10000, 10), +) +WISH_SCAN_TICKET_PERCENT = 10 GAME_LABELS = { "prize_draw": "抽奖", "lottery": "彩票", @@ -59,6 +79,12 @@ TRANSFER_MARKED = re.compile( r"(?:金额|摩拉|金币|coin|coins|:|:)\s*(?P\d{1,6})$", re.IGNORECASE, ) +TAFFY_SAY_SCHEDULE = re.compile( + r"^(?:定时|预约|schedule)\s+" + r"(?P