fix: bound chat latency and clarify blackjack flow

This commit is contained in:
JWeBot Contributors
2026-10-15 13:37:57 +08:00
parent f7fedc54e4
commit 0731e5cebb
25 changed files with 364 additions and 53 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-config"
version = "0.6.1a0+1015"
version = "0.6.2a0+1015"
description = "Profile-based configuration compiler for JWeBot"
requires-python = ">=3.9"
dependencies = [
@@ -36,4 +36,4 @@ __all__ = [
"write_default_profile",
]
__version__ = "0.6.1a0+1015"
__version__ = "0.6.2a0+1015"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-extension-host"
version = "0.6.1a0+1015"
version = "0.6.2a0+1015"
description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.1a0+1015",
"jwebot-kernel==0.6.1a0+1015",
"jwebot-sdk==0.6.2a0+1015",
"jwebot-kernel==0.6.2a0+1015",
]
[project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request",
]
__version__ = "0.6.1a0+1015"
__version__ = "0.6.2a0+1015"
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "0.6.1a0+1015"
version = "0.6.2a0+1015"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==0.6.1a0+1015",
"jwebot-sdk==0.6.1a0+1015",
"jwebot-official-extensions==0.6.1a0+1015",
"jwebot-config==0.6.2a0+1015",
"jwebot-sdk==0.6.2a0+1015",
"jwebot-official-extensions==0.6.2a0+1015",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "0.6.1a0+1015"
__version__ = "0.6.2a0+1015"
@@ -92,6 +92,10 @@ MAX_REPLY_CHARS = 900
MAX_CONTEXT_TURNS = 6
MAX_OUTBOX_ATTEMPTS = 3
DIRECT_CHAT_GENERATION_WORKERS = 2
DIRECT_API_SOCKET_TIMEOUT_SECONDS = 18.0
DIRECT_API_WALL_TIMEOUT_SECONDS = 30.0
DIRECT_API_MAX_RESPONSE_BYTES = 2_000_000
DIRECT_API_READ_CHUNK_BYTES = 65_536
FAST_ACKNOWLEDGEMENT = "喵......"
PROFILE_MIN_AUTOMATIC_OBSERVATIONS = 3
PROFILE_MAX_FACTS = 12
@@ -626,6 +630,59 @@ def deepseek_response_content(raw: bytes) -> str:
return str(content)
def read_direct_api_response(
response: Any,
*,
wall_timeout_seconds: float = DIRECT_API_WALL_TIMEOUT_SECONDS,
max_bytes: int = DIRECT_API_MAX_RESPONSE_BYTES,
) -> bytes:
"""Read an SSE response with an absolute deadline, not an idle timeout."""
deadline = time.monotonic() + wall_timeout_seconds
chunks: list[bytes] = []
total = 0
stream_tail = b""
done_marker = re.compile(
rb"(?:^|\r?\n)data:\s*\[DONE\]\s*(?:\r?\n|$)"
)
socket_connection = getattr(
getattr(getattr(response, "fp", None), "raw", None),
"_sock",
None,
)
read_chunk = getattr(response, "read1", response.read)
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise DeepSeekUnavailable("direct API exceeded total timeout")
if socket_connection is not None:
socket_connection.settimeout(
max(
0.001,
min(DIRECT_API_SOCKET_TIMEOUT_SECONDS, remaining),
)
)
try:
chunk = read_chunk(
min(DIRECT_API_READ_CHUNK_BYTES, max_bytes - total + 1)
)
except (TimeoutError, OSError) as error:
raise DeepSeekUnavailable("direct API transport failure") from error
if not chunk:
break
if not isinstance(chunk, bytes):
raise DeepSeekUnavailable("direct API returned invalid bytes")
total += len(chunk)
if total > max_bytes:
raise DeepSeekUnavailable("direct API response exceeded size limit")
chunks.append(chunk)
stream_scan = stream_tail + chunk
if done_marker.search(stream_scan):
break
stream_tail = stream_scan[-64:]
return b"".join(chunks)
def load_environment_file(path: Path = ENVIRONMENT_FILE) -> None:
"""Load the active generated profile, then the legacy compatibility file."""
@@ -3422,8 +3479,11 @@ class DeepSeekClient:
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=18) as response:
raw = response.read(2_000_000)
with urllib.request.urlopen(
request,
timeout=DIRECT_API_SOCKET_TIMEOUT_SECONDS,
) as response:
raw = read_direct_api_response(response)
except urllib.error.HTTPError as error:
raise DeepSeekUnavailable(f"direct API HTTP {error.code}") from error
except (urllib.error.URLError, TimeoutError, OSError) as error:
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-official-extensions"
version = "0.6.1a0+1015"
version = "0.6.2a0+1015"
description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.1a0+1015",
"jwebot-sdk==0.6.2a0+1015",
]
[tool.setuptools.packages.find]
@@ -62,4 +62,4 @@ __all__ = [
"render_feature_list",
]
__version__ = "0.6.1a0+1015"
__version__ = "0.6.2a0+1015"
@@ -6,7 +6,7 @@ from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'single-player-games'
AUTHOR = 'jwebot'
DESCRIPTION = '本人虚拟金币账户、抽奖、彩票和单人21点'
HELP_ENTRIES = ('/金币 /wallet;/抽奖 [金额] /draw;/买彩票 [号码] [金额] /lottery', '/21点 开始|要牌|停牌|状态|结束;/blackjack start|hit|stand|status|end', '/游戏帮助;/games')
HELP_ENTRIES = ('/金币 /wallet;/抽奖 [金额] /draw;/买彩票 [号码] [金额] /lottery', '/21点 开始 [金额](金额=本局下注,默认50,空格可省略)', '/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end', '/游戏帮助;/games')
COMMANDS = ('/金币', '/wallet', '/抽奖', '/draw', '/买彩票', '/lottery', '/21点', '/二十一点', '/blackjack', '/游戏帮助', '/games')
@@ -123,7 +123,8 @@ LOCAL_EXTENSIONS = (
description="本人虚拟金币账户、抽奖、彩票和单人21点",
help_entries=(
"/金币 /wallet;/抽奖 [金额] /draw;/买彩票 [号码] [金额] /lottery",
"/21点 开始|要牌|停牌|状态|结束;/blackjack start|hit|stand|status|end",
"/21点 开始 [金额](金额=本局下注,默认50,空格可省略)",
"/21点 要牌|停牌|状态|结束;/blackjack hit|stand|status|end",
"/游戏帮助;/games",
),
command_kinds=("wallet", "prize_draw", "lottery", "blackjack", "game_rules"),
@@ -11,6 +11,23 @@ from typing import Any
INITIAL_COINS = 1000
MAX_BET = 100
GAME_TIMEOUT_SECONDS = 600
BLACKJACK_ACTIONS = {
"要牌": "hit",
"hit": "hit",
"停牌": "stand",
"stand": "stand",
"状态": "status",
"status": "status",
"结束": "end",
"end": "end",
}
BLACKJACK_START = re.compile(
r"^(?:开始|start)(?P<bet>\d{1,3})?$"
)
BLACKJACK_COMBINED_ACTION = re.compile(
r"^(?:开始|start)(?P<bet>\d{1,3})?"
r"(?:要牌|hit|停牌|stand)$"
)
def card_label(card: int) -> str:
@@ -37,8 +54,9 @@ class PlayerGames:
return (
"单人游戏:每人首次使用获 1000 虚拟金币;每次下注 2~100。\n"
"/金币 /wallet;/抽奖 [金额] /draw;/买彩票 [000~999] [金额] /lottery;"
"/21点 开始 [金额] /blackjack start。\n"
"21点:/21点 要牌|停牌|状态|结束(hit|stand|status|end);"
"21点先用 /21点 开始 [金额] 进场,金额是本局下注,默认50;空格可省略。\n"
"看到牌后再单独发送 /21点 要牌|停牌|状态|结束"
"(/blackjack hit|stand|status|end);"
"每人最多一局,10分钟无操作视为认输,结束不退下注。\n"
"抽奖:5%返5倍、20%返2倍,其余无返还;彩票命中返500倍;"
"21点获胜返2倍、平局返下注、自然21点返2.5倍向下取整,庄家软17停牌。"
@@ -64,25 +82,40 @@ class PlayerGames:
number = int(match[1]) if match[1] is not None else None
bet = int(match[2] or 10)
elif kind == "blackjack":
parts = arguments.casefold().split()
action = parts[0] if parts else "状态"
actions = {"开始": "start", "start": "start", "要牌": "hit", "hit": "hit",
"停牌": "stand", "stand": "stand", "状态": "status", "status": "status",
"结束": "end", "end": "end"}
action = actions.get(action, "")
if not action or (action != "start" and len(parts) > 1) or len(parts) > 2:
return "用法:/21点 开始 [金额]|要牌|停牌|状态|结束(/blackjack start|hit|stand|status|end)"
if action == "start":
if len(parts) == 2 and not re.fullmatch(r"\d{1,3}", parts[1]):
return "21点下注必须为 2~100 的整数金币"
bet = int(parts[1]) if len(parts) == 2 else 50
compacted = re.sub(r"\s+", "", arguments.casefold()) or "状态"
start = BLACKJACK_START.fullmatch(compacted)
if start is not None:
action = "start"
bet = int(start["bet"] or 50)
else:
action = BLACKJACK_ACTIONS.get(compacted, "")
bet = 0
if not action:
combined = BLACKJACK_COMBINED_ACTION.fullmatch(compacted)
if combined is not None:
amount = combined["bet"] or "50"
return (
"21点要分两步:先发送 /21点 开始 "
f"{amount}({amount} 是本局下注);看到牌后再单独发送 "
"/21点 要牌 或 /21点 停牌"
)
return (
"用法:先 /21点 开始 [2~100](金额是本局下注,默认50);"
"开局后再单独发送 /21点 要牌|停牌|状态|结束;空格可省略"
)
else:
bet = 0
if kind in {"prize_draw", "lottery"} or kind == "blackjack" and action == "start":
if not 2 <= bet <= MAX_BET:
return "下注必须为 2~100 的整数金币"
person = self.ledger.resolve_person(
str(row["group_fingerprint"]),
str(row["sender"]),
)
player_label = self.display_label(person["display_name"])
def blackjack_reply(text: str) -> str:
return f"{player_label}:{text}"
def execute(account: dict[str, Any]) -> str:
expired = ""
@@ -97,11 +130,15 @@ class PlayerGames:
f"已结算胜 {account['wins']} / 负 {account['losses']} / 平 {account['ties']}。"
"首次赠送1000仅发一次;/游戏帮助 查看规则")
if kind == "blackjack" and action in {"hit", "stand", "status", "end"}:
return expired + self.play_blackjack(account, action)
return blackjack_reply(
expired + self.play_blackjack(account, action)
)
if account.get("game"):
return expired + "你已有一局21点,请先 /21点 停牌 或 /21点 结束"
message = expired + "你已有一局21点,请先 /21点 停牌 或 /21点 结束"
return blackjack_reply(message) if kind == "blackjack" else message
if account["balance"] < bet:
return expired + f"金币不足:余额 {account['balance']},本次需要 {bet}"
message = expired + f"金币不足:余额 {account['balance']},本次需要 {bet}"
return blackjack_reply(message) if kind == "blackjack" else message
account["balance"] -= bet
if kind == "blackjack":
deck = list(range(52))
@@ -110,8 +147,10 @@ class PlayerGames:
"dealer": deck[2:4], "updated_at": time.time()}
account["game"] = game
if hand_value(game["player"]) == 21 or hand_value(game["dealer"]) == 21:
return expired + self.finish_blackjack(account, natural=True)
return expired + self.show_hand(game)
return blackjack_reply(
expired + self.finish_blackjack(account, natural=True)
)
return blackjack_reply(expired + self.show_hand(game))
if kind == "prize_draw":
roll = self.rng.randrange(100)
payout = bet * (5 if roll < 5 else 2 if roll < 25 else 0)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-sdk"
version = "0.6.1a0+1015"
version = "0.6.2a0+1015"
description = "Typed, capability-limited extension API for JWeBot plugins"
requires-python = ">=3.9"
dependencies = []
@@ -38,4 +38,4 @@ __all__ = [
"SingleGroupContext",
]
__version__ = "0.6.1a0+1015"
__version__ = "0.6.2a0+1015"