feat: add CS Major autochess tournament
check / local-gate (push) Failing after 2m1s

This commit is contained in:
JWeBot Contributors
2026-10-15 15:17:32 +08:00
parent f07ae653c7
commit e713ff369a
26 changed files with 3901 additions and 20 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-config"
version = "0.6.3a0+1015"
version = "0.6.4a0+1015"
description = "Profile-based configuration compiler for JWeBot"
requires-python = ">=3.9"
dependencies = [
@@ -36,4 +36,4 @@ __all__ = [
"write_default_profile",
]
__version__ = "0.6.3a0+1015"
__version__ = "0.6.4a0+1015"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-extension-host"
version = "0.6.3a0+1015"
version = "0.6.4a0+1015"
description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.3a0+1015",
"jwebot-kernel==0.6.3a0+1015",
"jwebot-sdk==0.6.4a0+1015",
"jwebot-kernel==0.6.4a0+1015",
]
[project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request",
]
__version__ = "0.6.3a0+1015"
__version__ = "0.6.4a0+1015"
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "0.6.3a0+1015"
version = "0.6.4a0+1015"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==0.6.3a0+1015",
"jwebot-sdk==0.6.3a0+1015",
"jwebot-official-extensions==0.6.3a0+1015",
"jwebot-config==0.6.4a0+1015",
"jwebot-sdk==0.6.4a0+1015",
"jwebot-official-extensions==0.6.4a0+1015",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "0.6.3a0+1015"
__version__ = "0.6.4a0+1015"
@@ -1070,6 +1070,71 @@ class Ledger:
ON group_lottery_entries(
group_fingerprint, round_number, created_at, person_key
);
CREATE TABLE IF NOT EXISTS autochess_runs (
group_fingerprint TEXT NOT NULL,
run_id TEXT NOT NULL,
season_number INTEGER NOT NULL,
state TEXT NOT NULL,
host_person_key TEXT NOT NULL,
state_json TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (group_fingerprint, run_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS autochess_one_active_run
ON autochess_runs(group_fingerprint)
WHERE state IN ('registration', 'active');
CREATE TABLE IF NOT EXISTS autochess_action_replay (
group_fingerprint TEXT NOT NULL,
event_id TEXT NOT NULL,
run_id TEXT NOT NULL,
person_key TEXT NOT NULL,
command TEXT NOT NULL,
response_text TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (group_fingerprint, event_id)
);
CREATE TABLE IF NOT EXISTS autochess_story_logs (
group_fingerprint TEXT NOT NULL,
run_id TEXT NOT NULL,
story_id TEXT NOT NULL,
round_number INTEGER NOT NULL,
phase TEXT NOT NULL,
story_type TEXT NOT NULL,
story_text TEXT NOT NULL,
facts_json TEXT NOT NULL,
source_event_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (group_fingerprint, run_id, story_id),
FOREIGN KEY (group_fingerprint, run_id)
REFERENCES autochess_runs(group_fingerprint, run_id)
ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS autochess_battles (
group_fingerprint TEXT NOT NULL,
run_id TEXT NOT NULL,
battle_id TEXT NOT NULL,
round_number INTEGER NOT NULL,
phase TEXT NOT NULL,
battle_type TEXT NOT NULL,
player_a TEXT NOT NULL,
player_b TEXT NOT NULL,
result_json TEXT NOT NULL,
source_event_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (group_fingerprint, run_id, battle_id),
FOREIGN KEY (group_fingerprint, run_id)
REFERENCES autochess_runs(group_fingerprint, run_id)
ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS autochess_story_run_round
ON autochess_story_logs(
group_fingerprint, run_id, round_number, story_id
);
CREATE INDEX IF NOT EXISTS autochess_battle_run_round
ON autochess_battles(
group_fingerprint, run_id, round_number, battle_id
);
CREATE INDEX IF NOT EXISTS events_group_created
ON events(group_fingerprint, created_at);
CREATE INDEX IF NOT EXISTS outbox_due
@@ -3634,6 +3699,250 @@ class Ledger:
self.connection.execute("ROLLBACK")
raise
def _autochess_action_replay(
self,
group: str,
event_id: str,
person_key: str,
command: str,
) -> str | None:
previous = self.connection.execute(
"""
SELECT person_key, command, response_text
FROM autochess_action_replay
WHERE group_fingerprint = ? AND event_id = ?
""",
(group, event_id),
).fetchone()
if previous is None:
return None
if previous["person_key"] != person_key or previous["command"] != command:
raise LocalCommandError("同一事件不能变更自走棋玩家或指令")
return str(previous["response_text"])
def _load_autochess_run(
self,
group: str,
) -> tuple[sqlite3.Row | None, dict[str, Any] | None]:
row = self.connection.execute(
"""
SELECT * FROM autochess_runs
WHERE group_fingerprint = ?
ORDER BY
CASE WHEN state IN ('registration', 'active') THEN 0 ELSE 1 END,
season_number DESC
LIMIT 1
""",
(group,),
).fetchone()
if row is None:
return None, None
try:
state = json.loads(str(row["state_json"]))
except json.JSONDecodeError as error:
raise LocalCommandError("自走棋状态无效") from error
if not isinstance(state, dict):
raise LocalCommandError("自走棋状态无效")
return row, state
@staticmethod
def _validate_autochess_state(state: dict[str, Any]) -> None:
status = state.get("status")
if status not in {"registration", "active", "completed", "cancelled"}:
raise LocalCommandError("自走棋赛事状态无效")
if (
not isinstance(state.get("run_id"), str)
or not state["run_id"]
or type(state.get("season_number")) is not int
or int(state["season_number"]) < 1
or not isinstance(state.get("host_person_key"), str)
or not state["host_person_key"]
or not isinstance(state.get("players"), dict)
):
raise LocalCommandError("自走棋赛事结构无效")
if status in {"active", "completed"}:
players = state["players"].values()
if any(
not isinstance(player, dict)
or not isinstance(player.get("board"), list)
or len(player["board"]) != 5
for player in players
):
raise LocalCommandError("自走棋比赛必须保持五人首发")
def apply_autochess_action(
self,
group: str,
sender: str,
event_id: str,
command: str,
action: Callable[
[dict[str, Any] | None, dict[str, str], int],
dict[str, Any],
],
) -> str:
"""Commit autochess state, replay, stories and battles atomically."""
person = self._player_identity(group, sender)
person_key = str(person["person_key"])
with self.lock:
self.connection.execute("BEGIN IMMEDIATE")
try:
replay = self._autochess_action_replay(
group,
event_id,
person_key,
command,
)
if replay is not None:
self.connection.execute("COMMIT")
return replay
stored, state = self._load_autochess_run(group)
next_season = int(
self.connection.execute(
"""
SELECT COALESCE(MAX(season_number), 0) + 1
FROM autochess_runs
WHERE group_fingerprint = ?
""",
(group,),
).fetchone()[0]
)
result = action(state, person, next_season)
if not isinstance(result, dict):
raise LocalCommandError("自走棋动作结果无效")
response = result.get("response")
new_state = result.get("state")
stories = result.get("stories", [])
battles = result.get("battles", [])
if not isinstance(response, str) or not response.strip():
raise LocalCommandError("自走棋响应无效")
if not isinstance(stories, list) or not isinstance(battles, list):
raise LocalCommandError("自走棋归档无效")
timestamp = utc_now()
run_id = str(stored["run_id"]) if stored is not None else ""
if new_state is not None:
if not isinstance(new_state, dict):
raise LocalCommandError("自走棋状态无效")
self._validate_autochess_state(new_state)
run_id = str(new_state["run_id"])
season_number = int(new_state["season_number"])
existing = self.connection.execute(
"""
SELECT created_at FROM autochess_runs
WHERE group_fingerprint = ? AND run_id = ?
""",
(group, run_id),
).fetchone()
created_at = (
str(existing["created_at"])
if existing is not None
else timestamp
)
self.connection.execute(
"""
INSERT INTO autochess_runs (
group_fingerprint, run_id, season_number, state,
host_person_key, state_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(group_fingerprint, run_id) DO UPDATE SET
season_number = excluded.season_number,
state = excluded.state,
host_person_key = excluded.host_person_key,
state_json = excluded.state_json,
updated_at = excluded.updated_at
""",
(
group,
run_id,
season_number,
str(new_state["status"]),
str(new_state["host_person_key"]),
canonical_json(new_state),
created_at,
timestamp,
),
)
for item in stories:
if (
not isinstance(item, dict)
or not isinstance(item.get("story_id"), str)
or not isinstance(item.get("text"), str)
or not isinstance(item.get("facts", {}), dict)
):
raise LocalCommandError("自走棋剧情归档无效")
self.connection.execute(
"""
INSERT INTO autochess_story_logs (
group_fingerprint, run_id, story_id,
round_number, phase, story_type, story_text,
facts_json, source_event_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
group,
run_id,
str(item["story_id"]),
int(item.get("round_number", 0)),
str(item.get("phase", "")),
str(item.get("story_type", "")),
str(item["text"]),
canonical_json(item.get("facts", {})),
event_id,
timestamp,
),
)
for item in battles:
if (
not isinstance(item, dict)
or not isinstance(item.get("battle_id"), str)
or not isinstance(item.get("result"), dict)
):
raise LocalCommandError("自走棋战斗归档无效")
self.connection.execute(
"""
INSERT INTO autochess_battles (
group_fingerprint, run_id, battle_id,
round_number, phase, battle_type, player_a,
player_b, result_json, source_event_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
group,
run_id,
str(item["battle_id"]),
int(item.get("round_number", 0)),
str(item.get("phase", "")),
str(item.get("battle_type", "")),
str(item.get("player_a", "")),
str(item.get("player_b", "")),
canonical_json(item["result"]),
event_id,
timestamp,
),
)
self.connection.execute(
"""
INSERT INTO autochess_action_replay (
group_fingerprint, event_id, run_id, person_key,
command, response_text, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
group,
event_id,
run_id,
person_key,
command,
response,
timestamp,
),
)
self.connection.execute("COMMIT")
return response
except BaseException:
self.connection.execute("ROLLBACK")
raise
def status(self) -> dict[str, Any]:
with self.lock:
events = self.connection.execute("SELECT COUNT(*) FROM events").fetchone()[0]
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-official-extensions"
version = "0.6.3a0+1015"
version = "0.6.4a0+1015"
description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.3a0+1015",
"jwebot-sdk==0.6.4a0+1015",
]
[tool.setuptools.packages.find]
@@ -1,5 +1,17 @@
"""Reviewed JWeBot local command adapters and broker implementation."""
from .cs_autochess import (
CARD_BY_ID,
COACH_BONUSES,
MAX_PLAYERS,
MIN_PLAYERS,
PLAYER_POOL,
CsMajorAutochess,
build_round_plan,
lineup_power,
matchup_modifier,
parse_autochess_action,
)
from .descriptors import (
DIRECT_KERNEL_COMMANDS,
DIRECT_KERNEL_COMMAND_KINDS,
@@ -35,6 +47,9 @@ from .steam import (
)
__all__ = [
"CARD_BY_ID",
"COACH_BONUSES",
"CsMajorAutochess",
"DICE_CHALLENGE_TIMEOUT_SECONDS",
"DEFAULT_STEAM_MIHOMO_PROXY_URL",
"DIRECT_KERNEL_COMMANDS",
@@ -43,8 +58,12 @@ __all__ = [
"LOCAL_COMMAND_ALIASES",
"LOCAL_EXTENSIONS",
"LOCAL_GAME_TIMEOUT_SECONDS",
"MAX_PLAYERS",
"MAX_RPS_PLAYERS",
"MIN_PLAYERS",
"OFFICIAL_AUTHOR",
"PLAYER_POOL",
"build_round_plan",
"KernelCommandDescriptor",
"LocalExtensionDescriptor",
"LocalExtensionError",
@@ -58,8 +77,11 @@ __all__ = [
"deterministic_int",
"local_command_parts",
"local_extension_command_token",
"lineup_power",
"matchup_modifier",
"parse_autochess_action",
"extension_for_kind",
"render_feature_list",
]
__version__ = "0.6.3a0+1015"
__version__ = "0.6.4a0+1015"
@@ -0,0 +1,37 @@
"""Generated reviewed adapter for cs-major-autochess."""
from jwebot_sdk import Event, ExtensionResult, ReplyPlan, SingleGroupContext
PROFILE = 'cs-major-autochess'
AUTHOR = 'jwebot'
DESCRIPTION = 'CS自走棋'
HELP_ENTRIES = ('/自走棋开始|报名',)
COMMANDS = ('/自走棋', '/cs自走棋', '/autochess', '/选', '/刷新', '/锁定', '/战术', '/升级教练', '/上阵', '/下阵', '/出售', '/阵容', '/赛程', '/结束回合')
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(),
},
)
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))
@@ -61,6 +61,20 @@ LOCAL_COMMAND_ALIASES = {
"人物": "person_info",
"个人属性": "person_info",
"person": "person_info",
"自走棋": "cs_autochess",
"cs自走棋": "cs_autochess",
"autochess": "cs_autochess",
"选": "cs_autochess",
"刷新": "cs_autochess",
"锁定": "cs_autochess",
"战术": "cs_autochess",
"升级教练": "cs_autochess",
"上阵": "cs_autochess",
"下阵": "cs_autochess",
"出售": "cs_autochess",
"阵容": "cs_autochess",
"赛程": "cs_autochess",
"结束回合": "cs_autochess",
}
@@ -141,6 +155,13 @@ LOCAL_EXTENSIONS = (
"game_rules",
),
),
LocalExtensionDescriptor(
extension_id="cs-major-autochess",
author=JWEBOT_AUTHOR,
description="CS自走棋",
help_entries=("/自走棋开始|报名",),
command_kinds=("cs_autochess",),
),
LocalExtensionDescriptor(
extension_id="person-directory",
author=OFFICIAL_AUTHOR,
@@ -14,6 +14,7 @@ from datetime import datetime, timezone
from typing import Any, Callable
from zoneinfo import ZoneInfo
from .cs_autochess import CsMajorAutochess
from .descriptors import LOCAL_COMMAND_ALIASES, render_feature_list
from .player_games import PlayerGames
@@ -107,6 +108,7 @@ class LocalCommandEngine:
self.profile_fact_is_safe = profile_fact_is_safe
self.profile_expiry = profile_expiry
self.players = PlayerGames(ledger, person_display_label)
self.autochess = CsMajorAutochess(ledger)
def handle(self, row: dict[str, Any], event: dict[str, Any]) -> str:
question = str(row["question"])
@@ -128,6 +130,8 @@ class LocalCommandEngine:
"game_rules",
}:
return self.reply_cleaner(self.players.handle(row, kind, arguments))
if kind == "cs_autochess":
return self.reply_cleaner(self.autochess.handle(row))
if kind == "dice":
return self.reply_cleaner(self.dice(row, arguments))
if kind == "dice_challenge":
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-sdk"
version = "0.6.3a0+1015"
version = "0.6.4a0+1015"
description = "Typed, capability-limited extension API for JWeBot plugins"
requires-python = ">=3.9"
dependencies = []
@@ -38,4 +38,4 @@ __all__ = [
"SingleGroupContext",
]
__version__ = "0.6.3a0+1015"
__version__ = "0.6.4a0+1015"