This commit is contained in:
@@ -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,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"
|
||||
|
||||
+37
@@ -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))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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":
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user