@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## alpha 0.6.10.1015
|
||||||
|
|
||||||
|
- Add virtual coin loans with `/贷款 [金额]` and `/loan amount`, plus
|
||||||
|
repayments through `/还款 [金额|全部]` and `/repay`. Loans are stored in
|
||||||
|
the same group-scoped virtual coin account, capped at 500 coins, limited to
|
||||||
|
one active loan per account, and replay-safe through the existing ledger
|
||||||
|
transaction path.
|
||||||
|
- Split loan command ownership into a dedicated reviewed `coin-loan` local
|
||||||
|
extension so the single-player game plugin stays under the command-count
|
||||||
|
validation limit.
|
||||||
|
|
||||||
## alpha 0.6.9.1015
|
## alpha 0.6.9.1015
|
||||||
|
|
||||||
- Move CS Major autochess public game data out of `cs_autochess.py` and into
|
- Move CS Major autochess public game data out of `cs_autochess.py` and into
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
|
|||||||
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
|
||||||
并通过带视觉确认的 GUI 发送链路回复。
|
并通过带视觉确认的 GUI 发送链路回复。
|
||||||
|
|
||||||
当前版本:`alpha 0.6.9.1015`。
|
当前版本:`alpha 0.6.10.1015`。
|
||||||
|
|
||||||
0.6.1 新增本地上下文总结、人物与别名索引、单人虚拟金币游戏,并增加
|
0.6.1 新增本地上下文总结、人物与别名索引、单人虚拟金币游戏,并增加
|
||||||
两秒画面复核以补抓遗漏的消息变化。命令和游戏管理规则见
|
两秒画面复核以补抓遗漏的消息变化。命令和游戏管理规则见
|
||||||
@@ -41,6 +41,8 @@ D 牌、锁店、调阵容和战术窗口会在轮次之间自然提示。
|
|||||||
MR15/加时参数都由数据文件驱动,启动时会校验字段、ID 和素材数量。
|
MR15/加时参数都由数据文件驱动,启动时会校验字段、ID 和素材数量。
|
||||||
同版新增虚拟金币转账:`/转账 对方 金额` 会在同一事务内扣款入账,
|
同版新增虚拟金币转账:`/转账 对方 金额` 会在同一事务内扣款入账,
|
||||||
收款人需先发送 `/金币` 初始化账户。
|
收款人需先发送 `/金币` 初始化账户。
|
||||||
|
0.6.10 新增虚拟金币贷款:`/贷款 [金额]` 最多借入 500 金币,`/还款`
|
||||||
|
可按金额或全部归还;每个账户同一时间最多一笔未还贷款。
|
||||||
|
|
||||||
### 运行流程
|
### 运行流程
|
||||||
|
|
||||||
@@ -108,6 +110,7 @@ PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
|
|||||||
/人物列表 [页码](/people);/人物 [名称或别名](/person)
|
/人物列表 [页码](/people);/人物 [名称或别名](/person)
|
||||||
/金币(/wallet);/抽奖 [金额](/draw)
|
/金币(/wallet);/抽奖 [金额](/draw)
|
||||||
/转账 对方 金额(/transfer name amount)
|
/转账 对方 金额(/transfer name amount)
|
||||||
|
/贷款 [金额](/loan amount);/还款 [金额|全部](/repay)
|
||||||
/彩票 开始|报名|当前状态|开奖|取消(/lottery)
|
/彩票 开始|报名|当前状态|开奖|取消(/lottery)
|
||||||
/21点 开始 [1~500];开局后 /21点 要牌|停牌|状态|结束
|
/21点 开始 [1~500];开局后 /21点 要牌|停牌|状态|结束
|
||||||
/游戏记录 [页码](/gamehistory)
|
/游戏记录 [页码](/gamehistory)
|
||||||
@@ -284,7 +287,7 @@ visually confirmed GUI bridge.
|
|||||||
Current version:
|
Current version:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
alpha 0.6.9.1015
|
alpha 0.6.10.1015
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runtime Flow
|
## Runtime Flow
|
||||||
@@ -387,7 +390,8 @@ single-player virtual coin games. Chinese aliases share the same implementations
|
|||||||
Accounts receive 1000 virtual coins once; funds, game state and event replay
|
Accounts receive 1000 virtual coins once; funds, game state and event replay
|
||||||
results commit together. `/transfer name amount` moves virtual coins between
|
results commit together. `/transfer name amount` moves virtual coins between
|
||||||
initialized group-scoped accounts in one ledger transaction. OCR display aliases
|
initialized group-scoped accounts in one ledger transaction. OCR display aliases
|
||||||
identify group-scoped people; native WeChat account IDs are unavailable. See
|
identify group-scoped people; `/loan amount` and `/repay` manage one capped
|
||||||
|
virtual coin loan per account. Native WeChat account IDs are unavailable. See
|
||||||
[plugin rules and capture reconciliation](docs/local-plugins-0.6.1.md).
|
[plugin rules and capture reconciliation](docs/local-plugins-0.6.1.md).
|
||||||
Version 0.6.2 adds a 30-second total deadline to direct DeepSeek streams so an
|
Version 0.6.2 adds a 30-second total deadline to direct DeepSeek streams so an
|
||||||
unfinished upstream response cannot occupy both generation workers for about
|
unfinished upstream response cannot occupy both generation workers for about
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ OpenClaw;中文、英文命令对应同一实现,英文不区分大小写,
|
|||||||
| context-summary | `/上下文 [100|200|300]` | `/context [100|200|300]` |
|
| context-summary | `/上下文 [100|200|300]` | `/context [100|200|300]` |
|
||||||
| single-player-games | `/金币` | `/wallet` |
|
| single-player-games | `/金币` | `/wallet` |
|
||||||
| coin-transfer | `/转账 对方 金额` | `/transfer name amount` |
|
| coin-transfer | `/转账 对方 金额` | `/transfer name amount` |
|
||||||
|
| coin-loan | `/贷款 [金额]`、`/还款 [金额\|全部]` | `/loan amount`、`/repay [amount\|all]` |
|
||||||
| single-player-games | `/抽奖 [金额]` | `/draw [金额]` |
|
| single-player-games | `/抽奖 [金额]` | `/draw [金额]` |
|
||||||
| single-player-games | `/彩票 开始/报名/当前状态/开奖/取消` | `/lottery start/join/status/draw/cancel` |
|
| single-player-games | `/彩票 开始/报名/当前状态/开奖/取消` | `/lottery start/join/status/draw/cancel` |
|
||||||
| single-player-games | `/21点 开始 [金额]`(空格可省略) | `/blackjack start [金额]`(空格可省略) |
|
| single-player-games | `/21点 开始 [金额]`(空格可省略) | `/blackjack start [金额]`(空格可省略) |
|
||||||
@@ -52,7 +53,8 @@ person_key。相同人物的已配置别名共用金币和牌局,其他群使
|
|||||||
昵称识别不是账号身份认证。修改 canonical_name 会改变当前人物作用域;
|
昵称识别不是账号身份认证。修改 canonical_name 会改变当前人物作用域;
|
||||||
应优先追加别名而不是重命名。除 `/转账` 外,用户不能通过游戏命令
|
应优先追加别名而不是重命名。除 `/转账` 外,用户不能通过游戏命令
|
||||||
指定他人的账户;转账只支持同群虚拟金币账户,收款人需先发送 `/金币`
|
指定他人的账户;转账只支持同群虚拟金币账户,收款人需先发送 `/金币`
|
||||||
初始化。虚拟金币不能充值、赠币或兑换真实资产。
|
初始化。`/贷款` 和 `/还款` 只作用于本人账户。虚拟金币不能充值、
|
||||||
|
赠币或兑换真实资产。
|
||||||
|
|
||||||
## 虚拟金币与游戏管理
|
## 虚拟金币与游戏管理
|
||||||
|
|
||||||
@@ -65,6 +67,10 @@ person_key。相同人物的已配置别名共用金币和牌局,其他群使
|
|||||||
转账、响应记录和重试去重写在同一个 ledger 事务内,同一 event_id
|
转账、响应记录和重试去重写在同一个 ledger 事务内,同一 event_id
|
||||||
不会重复扣款。
|
不会重复扣款。
|
||||||
|
|
||||||
|
`/贷款 [金额]` 会给本人账户借入 1~500 金币;同一账户同一时间最多
|
||||||
|
一笔未还贷款。`/还款 [金额|全部]` 从当前金币余额中扣除并减少未还
|
||||||
|
贷款,借款、还款、响应记录和重试去重同样写在一个 ledger 事务内。
|
||||||
|
|
||||||
抽奖默认下注 10:5% 概率返还五倍下注,20% 返还两倍,其余无返还。
|
抽奖默认下注 10:5% 概率返还五倍下注,20% 返还两倍,其余无返还。
|
||||||
alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数字顺序无关,
|
alpha 0.6.3 将旧的个人即时彩票替换为群内多人轮次。三位数字顺序无关,
|
||||||
下注范围 1~500,命中 1/2/3 个分别返还 50%/120%/300%。完整流程见
|
下注范围 1~500,命中 1/2/3 个分别返还 50%/120%/300%。完整流程见
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# coin-loan
|
||||||
|
|
||||||
|
Author: `jwebot`
|
||||||
|
|
||||||
|
贷款
|
||||||
|
|
||||||
|
Help:
|
||||||
|
|
||||||
|
- `/贷款;/还款`
|
||||||
|
|
||||||
|
Registered commands:
|
||||||
|
|
||||||
|
- `/贷款`
|
||||||
|
- `/借款`
|
||||||
|
- `/loan`
|
||||||
|
- `/borrow`
|
||||||
|
- `/还款`
|
||||||
|
- `/repay`
|
||||||
|
|
||||||
|
The adapter has no direct Ledger, GUI, environment, shell, or network access.
|
||||||
|
It delegates to the reviewed `coin-loan` Kernel service profile
|
||||||
|
and returns a structured `ReplyPlan`.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
id = "coin-loan"
|
||||||
|
author = "jwebot"
|
||||||
|
description = "贷款"
|
||||||
|
help = ["/贷款;/还款"]
|
||||||
|
version = "1.0.0"
|
||||||
|
api_version = "1"
|
||||||
|
entrypoint = "coin_loan:plugin"
|
||||||
|
type = "command"
|
||||||
|
|
||||||
|
[capabilities]
|
||||||
|
commands = ["/贷款", "/借款", "/loan", "/borrow", "/还款", "/repay"]
|
||||||
|
storage = false
|
||||||
|
network_profiles = []
|
||||||
|
service_profiles = ["coin-loan"]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Generated reviewed adapter for coin-loan."""
|
||||||
|
|
||||||
|
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
|
PROFILE = 'coin-loan'
|
||||||
|
AUTHOR = 'jwebot'
|
||||||
|
DESCRIPTION = '贷款'
|
||||||
|
HELP_ENTRIES = ('/贷款;/还款',)
|
||||||
|
COMMANDS = ('/贷款', '/借款', '/loan', '/borrow', '/还款', '/repay')
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
@@ -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 coin_loan 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, ('/贷款;/还款',))
|
||||||
|
self.assertIn('/贷款', registered)
|
||||||
|
self.assertEqual(len(registered), 6)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-config"
|
name = "jwebot-config"
|
||||||
version = "0.6.9a0+1015"
|
version = "0.6.10a0+1015"
|
||||||
description = "Profile-based configuration compiler for JWeBot"
|
description = "Profile-based configuration compiler for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ __all__ = [
|
|||||||
"write_default_profile",
|
"write_default_profile",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.6.9a0+1015"
|
__version__ = "0.6.10a0+1015"
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-extension-host"
|
name = "jwebot-extension-host"
|
||||||
version = "0.6.9a0+1015"
|
version = "0.6.10a0+1015"
|
||||||
description = "Single-release, single-invocation process host for JWeBot extensions"
|
description = "Single-release, single-invocation process host for JWeBot extensions"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-sdk==0.6.9a0+1015",
|
"jwebot-sdk==0.6.10a0+1015",
|
||||||
"jwebot-kernel==0.6.9a0+1015",
|
"jwebot-kernel==0.6.10a0+1015",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ __all__ = [
|
|||||||
"process_request",
|
"process_request",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.6.9a0+1015"
|
__version__ = "0.6.10a0+1015"
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-kernel"
|
name = "jwebot-kernel"
|
||||||
version = "0.6.9a0+1015"
|
version = "0.6.10a0+1015"
|
||||||
description = "Stable routing and extension authority for JWeBot"
|
description = "Stable routing and extension authority for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-config==0.6.9a0+1015",
|
"jwebot-config==0.6.10a0+1015",
|
||||||
"jwebot-sdk==0.6.9a0+1015",
|
"jwebot-sdk==0.6.10a0+1015",
|
||||||
"jwebot-official-extensions==0.6.9a0+1015",
|
"jwebot-official-extensions==0.6.10a0+1015",
|
||||||
"tomli>=2; python_version < '3.11'",
|
"tomli>=2; python_version < '3.11'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -107,4 +107,4 @@ __all__ = [
|
|||||||
"verify_installed_release_signature",
|
"verify_installed_release_signature",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.6.9a0+1015"
|
__version__ = "0.6.10a0+1015"
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-official-extensions"
|
name = "jwebot-official-extensions"
|
||||||
version = "0.6.9a0+1015"
|
version = "0.6.10a0+1015"
|
||||||
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
description = "Reviewed local command adapters and broker implementation for JWeBot"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jwebot-sdk==0.6.9a0+1015",
|
"jwebot-sdk==0.6.10a0+1015",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
@@ -90,4 +90,4 @@ __all__ = [
|
|||||||
"render_feature_list",
|
"render_feature_list",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.6.9a0+1015"
|
__version__ = "0.6.10a0+1015"
|
||||||
|
|||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
"""Generated reviewed adapter for coin-loan."""
|
||||||
|
|
||||||
|
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
|
||||||
|
|
||||||
|
|
||||||
|
PROFILE = 'coin-loan'
|
||||||
|
AUTHOR = 'jwebot'
|
||||||
|
DESCRIPTION = '贷款'
|
||||||
|
HELP_ENTRIES = ('/贷款;/还款',)
|
||||||
|
COMMANDS = ('/贷款', '/借款', '/loan', '/borrow', '/还款', '/repay')
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
@@ -44,6 +44,12 @@ LOCAL_COMMAND_ALIASES = {
|
|||||||
"wallet": "wallet",
|
"wallet": "wallet",
|
||||||
"转账": "transfer",
|
"转账": "transfer",
|
||||||
"transfer": "transfer",
|
"transfer": "transfer",
|
||||||
|
"贷款": "loan",
|
||||||
|
"借款": "loan",
|
||||||
|
"loan": "loan",
|
||||||
|
"borrow": "loan",
|
||||||
|
"还款": "repay",
|
||||||
|
"repay": "repay",
|
||||||
"抽奖": "prize_draw",
|
"抽奖": "prize_draw",
|
||||||
"draw": "prize_draw",
|
"draw": "prize_draw",
|
||||||
"彩票": "lottery",
|
"彩票": "lottery",
|
||||||
@@ -165,6 +171,13 @@ LOCAL_EXTENSIONS = (
|
|||||||
help_entries=("/转账 对方 金额;/transfer",),
|
help_entries=("/转账 对方 金额;/transfer",),
|
||||||
command_kinds=("transfer",),
|
command_kinds=("transfer",),
|
||||||
),
|
),
|
||||||
|
LocalExtensionDescriptor(
|
||||||
|
extension_id="coin-loan",
|
||||||
|
author=JWEBOT_AUTHOR,
|
||||||
|
description="贷款",
|
||||||
|
help_entries=("/贷款;/还款",),
|
||||||
|
command_kinds=("loan", "repay"),
|
||||||
|
),
|
||||||
LocalExtensionDescriptor(
|
LocalExtensionDescriptor(
|
||||||
extension_id="cs-major-autochess",
|
extension_id="cs-major-autochess",
|
||||||
author=JWEBOT_AUTHOR,
|
author=JWEBOT_AUTHOR,
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ class LocalCommandEngine:
|
|||||||
if kind in {
|
if kind in {
|
||||||
"wallet",
|
"wallet",
|
||||||
"transfer",
|
"transfer",
|
||||||
|
"loan",
|
||||||
|
"repay",
|
||||||
"prize_draw",
|
"prize_draw",
|
||||||
"lottery",
|
"lottery",
|
||||||
"blackjack",
|
"blackjack",
|
||||||
|
|||||||
+103
-2
@@ -18,6 +18,8 @@ MIN_BLACKJACK_BET = 1
|
|||||||
MAX_BLACKJACK_BET = 500
|
MAX_BLACKJACK_BET = 500
|
||||||
MIN_TRANSFER_AMOUNT = 1
|
MIN_TRANSFER_AMOUNT = 1
|
||||||
MAX_TRANSFER_AMOUNT = 100_000
|
MAX_TRANSFER_AMOUNT = 100_000
|
||||||
|
MIN_LOAN_AMOUNT = 1
|
||||||
|
MAX_LOAN_AMOUNT = 500
|
||||||
GAME_TIMEOUT_SECONDS = 600
|
GAME_TIMEOUT_SECONDS = 600
|
||||||
GAME_HISTORY_LIMIT = 50
|
GAME_HISTORY_LIMIT = 50
|
||||||
GAME_HISTORY_PAGE_SIZE = 5
|
GAME_HISTORY_PAGE_SIZE = 5
|
||||||
@@ -53,6 +55,18 @@ TRANSFER_MARKED = re.compile(
|
|||||||
def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
def normalize_player_account(account: dict[str, Any]) -> dict[str, Any]:
|
||||||
account.setdefault("balance", INITIAL_COINS)
|
account.setdefault("balance", INITIAL_COINS)
|
||||||
account.setdefault("game", None)
|
account.setdefault("game", None)
|
||||||
|
loan = account.get("loan")
|
||||||
|
try:
|
||||||
|
loan_principal = int(loan.get("principal", 0) or 0) if isinstance(loan, dict) else 0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
loan_principal = 0
|
||||||
|
if loan_principal <= 0:
|
||||||
|
account["loan"] = None
|
||||||
|
else:
|
||||||
|
account["loan"] = {
|
||||||
|
"principal": loan_principal,
|
||||||
|
"borrowed_at": float(loan.get("borrowed_at", time.time())),
|
||||||
|
}
|
||||||
account.setdefault("wins", 0)
|
account.setdefault("wins", 0)
|
||||||
account.setdefault("losses", 0)
|
account.setdefault("losses", 0)
|
||||||
account.setdefault("ties", 0)
|
account.setdefault("ties", 0)
|
||||||
@@ -137,6 +151,8 @@ class PlayerGames:
|
|||||||
"每人首次使用获 1000 虚拟金币;/金币 查看余额;/游戏记录 查看本人记录。\n"
|
"每人首次使用获 1000 虚拟金币;/金币 查看余额;/游戏记录 查看本人记录。\n"
|
||||||
"转账:/转账 对方 金额,或 /transfer name amount;"
|
"转账:/转账 对方 金额,或 /transfer name amount;"
|
||||||
"对方需先用 /金币 初始化账户。\n"
|
"对方需先用 /金币 初始化账户。\n"
|
||||||
|
"贷款:/贷款 [1~500] 借入金币;/还款 [金额|全部] 归还,"
|
||||||
|
"同一时间最多一笔未还贷款。\n"
|
||||||
"抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n"
|
"抽奖:/抽奖 [2~100],5%返5倍、20%返2倍,其余无返还。\n"
|
||||||
"多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];"
|
"多人彩票:/彩票 开始;/彩票 报名 3 4 7 [1~500];"
|
||||||
"/彩票 当前状态;/彩票 开奖。三位号码顺序无关,省略金额默认10;"
|
"/彩票 当前状态;/彩票 开奖。三位号码顺序无关,省略金额默认10;"
|
||||||
@@ -161,6 +177,8 @@ class PlayerGames:
|
|||||||
return self.group_lottery(row, arguments)
|
return self.group_lottery(row, arguments)
|
||||||
if kind == "transfer":
|
if kind == "transfer":
|
||||||
return self.transfer(row, arguments)
|
return self.transfer(row, arguments)
|
||||||
|
if kind in {"loan", "repay"}:
|
||||||
|
return self.loan_or_repay(row, kind, arguments)
|
||||||
if kind == "wallet" and arguments:
|
if kind == "wallet" and arguments:
|
||||||
return "用法:/金币(/wallet),查看本人的虚拟金币;/游戏帮助 查看规则"
|
return "用法:/金币(/wallet),查看本人的虚拟金币;/游戏帮助 查看规则"
|
||||||
if kind == "prize_draw":
|
if kind == "prize_draw":
|
||||||
@@ -233,9 +251,13 @@ class PlayerGames:
|
|||||||
expired = "上一局已超时,按认输结束;"
|
expired = "上一局已超时,按认输结束;"
|
||||||
if kind == "wallet":
|
if kind == "wallet":
|
||||||
state = "进行中" if account.get("game") else "无进行中的牌局"
|
state = "进行中" if account.get("game") else "无进行中的牌局"
|
||||||
return (f"{expired}金币 {account['balance']};21点 {state};"
|
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']}。"
|
f"已结算胜 {account['wins']} / 负 {account['losses']} / 平 {account['ties']}。"
|
||||||
"首次赠送1000仅发一次;/游戏帮助 查看规则")
|
"首次赠送1000仅发一次;/游戏帮助 查看规则"
|
||||||
|
)
|
||||||
if kind == "blackjack" and action in {"hit", "stand", "status", "end"}:
|
if kind == "blackjack" and action in {"hit", "stand", "status", "end"}:
|
||||||
active = account.get("game")
|
active = account.get("game")
|
||||||
index = int(active["index"]) if active else None
|
index = int(active["index"]) if active else None
|
||||||
@@ -366,6 +388,85 @@ class PlayerGames:
|
|||||||
render,
|
render,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def loan_or_repay(self, row: dict[str, Any], kind: str, arguments: str) -> str:
|
||||||
|
value = re.sub(r"\s+", "", arguments.casefold())
|
||||||
|
if kind == "loan":
|
||||||
|
if not re.fullmatch(r"\d{1,3}", value):
|
||||||
|
return "用法:/贷款 [1~500];同一时间最多一笔未还贷款"
|
||||||
|
amount = int(value)
|
||||||
|
if not MIN_LOAN_AMOUNT <= amount <= MAX_LOAN_AMOUNT:
|
||||||
|
return "贷款金额必须为 1~500 的整数金币"
|
||||||
|
else:
|
||||||
|
amount = 0
|
||||||
|
if value not in {"", "全部", "all"}:
|
||||||
|
if not re.fullmatch(r"\d{1,6}", value):
|
||||||
|
return "用法:/还款 [金额|全部]"
|
||||||
|
amount = int(value)
|
||||||
|
if amount <= 0:
|
||||||
|
return "还款金额必须为正整数金币"
|
||||||
|
|
||||||
|
group = str(row["group_fingerprint"])
|
||||||
|
sender = str(row["sender"])
|
||||||
|
person = self.ledger.resolve_person(group, sender)
|
||||||
|
player_label = self.display_label(person["display_name"])
|
||||||
|
|
||||||
|
def execute(account: dict[str, Any]) -> str:
|
||||||
|
normalize_player_account(account)
|
||||||
|
loan = account.get("loan")
|
||||||
|
if kind == "loan":
|
||||||
|
if loan:
|
||||||
|
return (
|
||||||
|
f"{player_label}:已有未还贷款 {loan['principal']} 金币;"
|
||||||
|
"请先 /还款 [金额|全部]"
|
||||||
|
)
|
||||||
|
account["balance"] += amount
|
||||||
|
account["loan"] = {
|
||||||
|
"principal": amount,
|
||||||
|
"borrowed_at": time.time(),
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
f"{player_label}:贷款 {amount} 金币已到账;"
|
||||||
|
f"当前金币 {account['balance']};未还贷款 {amount}"
|
||||||
|
)
|
||||||
|
if not loan:
|
||||||
|
return f"{player_label}:当前没有未还贷款"
|
||||||
|
debt = int(loan["principal"])
|
||||||
|
if amount <= 0:
|
||||||
|
pay = min(int(account["balance"]), debt)
|
||||||
|
else:
|
||||||
|
pay = min(amount, debt)
|
||||||
|
if pay <= 0:
|
||||||
|
return f"{player_label}:金币不足,无法还款;未还贷款 {debt}"
|
||||||
|
if pay > int(account["balance"]):
|
||||||
|
return (
|
||||||
|
f"{player_label}:金币不足,余额 {account['balance']},"
|
||||||
|
f"本次还款需要 {pay}"
|
||||||
|
)
|
||||||
|
account["balance"] -= pay
|
||||||
|
debt -= pay
|
||||||
|
if debt:
|
||||||
|
account["loan"] = {
|
||||||
|
"principal": debt,
|
||||||
|
"borrowed_at": float(loan.get("borrowed_at", time.time())),
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
f"{player_label}:已还款 {pay} 金币;"
|
||||||
|
f"剩余贷款 {debt};当前金币 {account['balance']}"
|
||||||
|
)
|
||||||
|
account["loan"] = None
|
||||||
|
return (
|
||||||
|
f"{player_label}:已还清贷款 {pay} 金币;"
|
||||||
|
f"当前金币 {account['balance']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.ledger.apply_player_action(
|
||||||
|
group,
|
||||||
|
sender,
|
||||||
|
str(row["event_id"]),
|
||||||
|
f"{kind}:{arguments}",
|
||||||
|
execute,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_lottery_entry(
|
def parse_lottery_entry(
|
||||||
arguments: str,
|
arguments: str,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jwebot-sdk"
|
name = "jwebot-sdk"
|
||||||
version = "0.6.9a0+1015"
|
version = "0.6.10a0+1015"
|
||||||
description = "Typed, capability-limited extension API for JWeBot plugins"
|
description = "Typed, capability-limited extension API for JWeBot plugins"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
@@ -38,4 +38,4 @@ __all__ = [
|
|||||||
"SingleGroupContext",
|
"SingleGroupContext",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.6.9a0+1015"
|
__version__ = "0.6.10a0+1015"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tool.jwebot]
|
[tool.jwebot]
|
||||||
release = "alpha 0.6.9.1015"
|
release = "alpha 0.6.10.1015"
|
||||||
production_deployments_enabled_by_default = false
|
production_deployments_enabled_by_default = false
|
||||||
|
|
||||||
[tool.jwebot.workspace]
|
[tool.jwebot.workspace]
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class LocalExtensionInventoryTests(unittest.TestCase):
|
|||||||
"dice": JWEBOT_AUTHOR,
|
"dice": JWEBOT_AUTHOR,
|
||||||
"coin": JWEBOT_AUTHOR,
|
"coin": JWEBOT_AUTHOR,
|
||||||
"coin-transfer": JWEBOT_AUTHOR,
|
"coin-transfer": JWEBOT_AUTHOR,
|
||||||
|
"coin-loan": JWEBOT_AUTHOR,
|
||||||
"random-number": JWEBOT_AUTHOR,
|
"random-number": JWEBOT_AUTHOR,
|
||||||
"draw-lot": JWEBOT_AUTHOR,
|
"draw-lot": JWEBOT_AUTHOR,
|
||||||
"daily-fortune": JWEBOT_AUTHOR,
|
"daily-fortune": JWEBOT_AUTHOR,
|
||||||
@@ -128,6 +129,7 @@ class LocalExtensionInventoryTests(unittest.TestCase):
|
|||||||
"person_memory",
|
"person_memory",
|
||||||
"memory_help",
|
"memory_help",
|
||||||
),
|
),
|
||||||
|
"coin-loan": ("loan", "repay"),
|
||||||
"server-status": ("local_status", "server_status"),
|
"server-status": ("local_status", "server_status"),
|
||||||
"dice": ("dice", "dice_challenge"),
|
"dice": ("dice", "dice_challenge"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -219,6 +219,33 @@ class Release063GameTests(unittest.TestCase):
|
|||||||
self.assertEqual(self.state()["balance"], 1000)
|
self.assertEqual(self.state()["balance"], 1000)
|
||||||
self.assertEqual(self.state("Bob")["balance"], 1000)
|
self.assertEqual(self.state("Bob")["balance"], 1000)
|
||||||
|
|
||||||
|
def test_loan_repay_and_replay_once(self):
|
||||||
|
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.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.assertEqual(self.state()["balance"], 1180)
|
||||||
|
self.assertEqual(self.state()["loan"]["principal"], 180)
|
||||||
|
|
||||||
|
done = self.command("/还款 全部")
|
||||||
|
self.assertIn("已还清贷款 180 金币", done)
|
||||||
|
self.assertEqual(self.state()["balance"], 1000)
|
||||||
|
self.assertIsNone(self.state()["loan"])
|
||||||
|
|
||||||
|
def test_loan_rejects_invalid_amounts_and_empty_repay(self):
|
||||||
|
self.assertIn("贷款金额必须为 1~500", self.command("/贷款 0"))
|
||||||
|
self.assertIn("贷款金额必须为 1~500", self.command("/loan 501"))
|
||||||
|
self.assertIn("当前没有未还贷款", self.command("/还款 全部"))
|
||||||
|
|
||||||
def test_open_round_and_history_survive_restart(self):
|
def test_open_round_and_history_survive_restart(self):
|
||||||
root = Path(self.temp.name)
|
root = Path(self.temp.name)
|
||||||
self.command("/彩票开始")
|
self.command("/彩票开始")
|
||||||
|
|||||||
Reference in New Issue
Block a user