Add coin leaderboard command
check / local-gate (push) Failing after 1m34s

This commit is contained in:
JWeBot Contributors
2026-10-15 19:00:29 +08:00
parent 148ae672f4
commit bf4f28f48e
26 changed files with 357 additions and 24 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-config"
version = "0.6.10a0+1015"
version = "0.6.11a0+1015"
description = "Profile-based configuration compiler for JWeBot"
requires-python = ">=3.9"
dependencies = [
@@ -36,4 +36,4 @@ __all__ = [
"write_default_profile",
]
__version__ = "0.6.10a0+1015"
__version__ = "0.6.11a0+1015"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-extension-host"
version = "0.6.10a0+1015"
version = "0.6.11a0+1015"
description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.10a0+1015",
"jwebot-kernel==0.6.10a0+1015",
"jwebot-sdk==0.6.11a0+1015",
"jwebot-kernel==0.6.11a0+1015",
]
[project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request",
]
__version__ = "0.6.10a0+1015"
__version__ = "0.6.11a0+1015"
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "0.6.10a0+1015"
version = "0.6.11a0+1015"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==0.6.10a0+1015",
"jwebot-sdk==0.6.10a0+1015",
"jwebot-official-extensions==0.6.10a0+1015",
"jwebot-config==0.6.11a0+1015",
"jwebot-sdk==0.6.11a0+1015",
"jwebot-official-extensions==0.6.11a0+1015",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "0.6.10a0+1015"
__version__ = "0.6.11a0+1015"
@@ -3665,6 +3665,114 @@ class Ledger:
"game_counts": dict(account["game_counts"]),
}
def player_coin_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)
loan = account.get("loan")
loan_principal = (
int(loan.get("principal", 0))
if isinstance(loan, dict)
else 0
)
records.append(
{
"display_name": labels.get(
person_key,
f"玩家 {person_key[:8]}",
),
"balance": int(account["balance"]),
"loan": loan_principal,
"updated_at": str(row["updated_at"]),
}
)
records.sort(
key=lambda item: (
-int(item["balance"]),
str(item["display_name"]).casefold(),
)
)
return records[: max(1, min(int(limit), 20))]
def transfer_player_coins(
self,
group: str,
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-official-extensions"
version = "0.6.10a0+1015"
version = "0.6.11a0+1015"
description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.10a0+1015",
"jwebot-sdk==0.6.11a0+1015",
]
[tool.setuptools.packages.find]
@@ -90,4 +90,4 @@ __all__ = [
"render_feature_list",
]
__version__ = "0.6.10a0+1015"
__version__ = "0.6.11a0+1015"
@@ -0,0 +1,45 @@
"""Generated reviewed adapter for coin-ranking."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'coin-ranking'
AUTHOR = 'jwebot'
DESCRIPTION = '富榜'
HELP_ENTRIES = ('/大富翁榜',)
COMMANDS = ('/金币排行', '/金币排行榜', '/大富翁榜', '/richlist')
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))
@@ -42,6 +42,10 @@ LOCAL_COMMAND_ALIASES = {
"context": "context_summary",
"金币": "wallet",
"wallet": "wallet",
"金币排行": "coin_ranking",
"金币排行榜": "coin_ranking",
"大富翁榜": "coin_ranking",
"richlist": "coin_ranking",
"转账": "transfer",
"transfer": "transfer",
"贷款": "loan",
@@ -171,6 +175,13 @@ LOCAL_EXTENSIONS = (
help_entries=("/转账 对方 金额;/transfer",),
command_kinds=("transfer",),
),
LocalExtensionDescriptor(
extension_id="coin-ranking",
author=JWEBOT_AUTHOR,
description="富榜",
help_entries=("/大富翁榜",),
command_kinds=("coin_ranking",),
),
LocalExtensionDescriptor(
extension_id="coin-loan",
author=JWEBOT_AUTHOR,
@@ -154,6 +154,7 @@ class LocalCommandEngine:
return self.reply_cleaner(self.person_index(row, kind, arguments))
if kind in {
"wallet",
"coin_ranking",
"transfer",
"loan",
"repay",
@@ -175,6 +175,8 @@ class PlayerGames:
return self.game_history(row, arguments)
if kind == "lottery":
return self.group_lottery(row, arguments)
if kind == "coin_ranking":
return self.coin_ranking(row, arguments)
if kind == "transfer":
return self.transfer(row, arguments)
if kind in {"loan", "repay"}:
@@ -316,6 +318,29 @@ class PlayerGames:
str(row["group_fingerprint"]), str(row["sender"]),
str(row["event_id"]), kind + ":" + arguments, execute)
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 "用法:/大富翁榜 或 /金币排行 [人数]"
limit = int(value or 10)
if not 1 <= limit <= 20:
return "金币排行人数必须为 1~20"
records = self.ledger.player_coin_ranking(
str(row["group_fingerprint"]),
limit=limit,
)
if not records:
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"{loan_text}"
)
return "\n".join(lines)
@staticmethod
def parse_transfer(
arguments: str,
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-sdk"
version = "0.6.10a0+1015"
version = "0.6.11a0+1015"
description = "Typed, capability-limited extension API for JWeBot plugins"
requires-python = ">=3.9"
dependencies = []
@@ -38,4 +38,4 @@ __all__ = [
"SingleGroupContext",
]
__version__ = "0.6.10a0+1015"
__version__ = "0.6.11a0+1015"