Release alpha 0.6.6 autochess flow
check / local-gate (push) Failing after 40s

This commit is contained in:
JWeBot Contributors
2026-10-15 17:10:28 +08:00
parent 9fff57134f
commit a74147e7f1
64 changed files with 1838 additions and 1060 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-config"
version = "0.6.5a0+1015"
version = "0.6.6a0+1015"
description = "Profile-based configuration compiler for JWeBot"
requires-python = ">=3.9"
dependencies = [
@@ -36,4 +36,4 @@ __all__ = [
"write_default_profile",
]
__version__ = "0.6.5a0+1015"
__version__ = "0.6.6a0+1015"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-extension-host"
version = "0.6.5a0+1015"
version = "0.6.6a0+1015"
description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.5a0+1015",
"jwebot-kernel==0.6.5a0+1015",
"jwebot-sdk==0.6.6a0+1015",
"jwebot-kernel==0.6.6a0+1015",
]
[project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request",
]
__version__ = "0.6.5a0+1015"
__version__ = "0.6.6a0+1015"
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "0.6.5a0+1015"
version = "0.6.6a0+1015"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==0.6.5a0+1015",
"jwebot-sdk==0.6.5a0+1015",
"jwebot-official-extensions==0.6.5a0+1015",
"jwebot-config==0.6.6a0+1015",
"jwebot-sdk==0.6.6a0+1015",
"jwebot-official-extensions==0.6.6a0+1015",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "0.6.5a0+1015"
__version__ = "0.6.6a0+1015"
@@ -61,6 +61,8 @@ from jwebot_official_extensions import (
extension_for_kind,
local_command_parts,
local_extension_command_token,
pack_reply_parts,
unpack_reply_parts,
)
from jwebot_official_extensions.steam import (
DEFAULT_STEAM_MIHOMO_PROXY_URL,
@@ -3760,7 +3762,16 @@ class Ledger:
or not isinstance(state.get("players"), dict)
):
raise LocalCommandError("自走棋赛事结构无效")
if status in {"active", "completed"}:
phase = str(state.get("phase", ""))
roster_required = (
status == "completed"
or (
status == "active"
and not phase.startswith("draft_")
and phase != "registration"
)
)
if roster_required:
players = state["players"].values()
if any(
not isinstance(player, dict)
@@ -4979,7 +4990,11 @@ class EdgeApplication:
error_type=type(error).__name__,
)
raise EdgeError("隔离插件暂时不可用") from error
if result.reply.kind is not PlanKind.TEXT:
if result.reply.kind is PlanKind.MULTI_TEXT:
reply_message = pack_reply_parts(list(result.reply.messages))
elif result.reply.kind is PlanKind.TEXT:
reply_message = result.reply.message
else:
raise EdgeError("隔离插件没有返回可见回复")
self.telemetry.elapsed(
"edge.extension.execute",
@@ -4989,7 +5004,7 @@ class EdgeApplication:
plugin_id=owner.extension_id,
mode="host",
)
return result.reply.message
return reply_message
if (
self.config.extension_mode == "host-only"
or (
@@ -5078,13 +5093,18 @@ class EdgeApplication:
"duplicate": True,
}
if row["response_text"]:
return {
parts = unpack_reply_parts(row["response_text"])
text = parts[0] if parts is not None else row["response_text"]
response = {
"status": "reply",
"route": row["route"],
"execution_path": stored_execution_path,
"text": row["response_text"],
"text": text,
"duplicate": True,
}
if parts is not None:
response["texts"] = parts
return response
return {
"status": "fallback_cloud",
"reason": "existing_incomplete_event",
@@ -5206,6 +5226,13 @@ class EdgeApplication:
status="local_completed",
response_text=reply,
)
parts = unpack_reply_parts(reply)
visible_reply = parts[0] if parts is not None else reply
reply_chars = (
sum(len(part) for part in parts)
if parts is not None
else len(reply)
)
self.telemetry.elapsed(
"edge.local.complete",
started,
@@ -5214,14 +5241,18 @@ class EdgeApplication:
route=route,
reason=reason,
execution_path=execution_path,
chars=len(reply),
chars=reply_chars,
messages=len(parts) if parts is not None else 1,
)
return {
response = {
"status": "reply",
"route": route,
"execution_path": execution_path,
"text": reply,
"text": visible_reply,
}
if parts is not None:
response["texts"] = parts
return response
alias_reply = self.ledger.local_alias_identity_reply(
str(row["group_fingerprint"]),
str(row["question"]),
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-official-extensions"
version = "0.6.5a0+1015"
version = "0.6.6a0+1015"
description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-sdk==0.6.5a0+1015",
"jwebot-sdk==0.6.6a0+1015",
]
[tool.setuptools.packages.find]
@@ -3,6 +3,7 @@
from .cs_autochess import (
CARD_BY_ID,
COACH_BONUSES,
COACH_POOL,
MAX_PLAYERS,
MIN_PLAYERS,
PLAYER_POOL,
@@ -32,6 +33,8 @@ from .local_commands import (
LocalCommandError,
deterministic_int,
local_command_parts,
pack_reply_parts,
unpack_reply_parts,
)
from .runtime import (
LocalExtensionError,
@@ -49,6 +52,7 @@ from .steam import (
__all__ = [
"CARD_BY_ID",
"COACH_BONUSES",
"COACH_POOL",
"CsMajorAutochess",
"DICE_CHALLENGE_TIMEOUT_SECONDS",
"DEFAULT_STEAM_MIHOMO_PROXY_URL",
@@ -79,9 +83,11 @@ __all__ = [
"local_extension_command_token",
"lineup_power",
"matchup_modifier",
"pack_reply_parts",
"parse_autochess_action",
"unpack_reply_parts",
"extension_for_kind",
"render_feature_list",
]
__version__ = "0.6.5a0+1015"
__version__ = "0.6.6a0+1015"
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
@@ -21,6 +21,14 @@ def _handler(context: SingleGroupContext, command: str):
"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")
File diff suppressed because it is too large Load Diff
@@ -25,6 +25,7 @@ MAX_RPS_PLAYERS = 8
BEIJING_TIMEZONE = ZoneInfo("Asia/Shanghai")
DICE_PATTERN = re.compile(r"^(?:(\d{1,2})[dD](\d{1,3}))?$")
RANDOM_NUMBER_PATTERN = re.compile(r"^(-?\d{1,6})\s+(-?\d{1,6})$")
MULTI_REPLY_MARKER = "\x1eJWEBOT_MULTI_REPLY_V1\x1e"
class LocalCommandError(RuntimeError):
@@ -53,6 +54,36 @@ def deterministic_int(seed: str, lower: int, upper: int) -> int:
return lower + value % (upper - lower + 1)
def pack_reply_parts(parts: list[str] | tuple[str, ...]) -> str:
cleaned = [str(part).strip() for part in parts if str(part).strip()]
if not cleaned:
raise LocalCommandError("本地命令没有可发送回复")
if len(cleaned) == 1:
return cleaned[0]
return MULTI_REPLY_MARKER + json.dumps(
cleaned,
ensure_ascii=False,
separators=(",", ":"),
)
def unpack_reply_parts(value: object) -> list[str] | None:
text = str(value or "")
if not text.startswith(MULTI_REPLY_MARKER):
return None
try:
payload = json.loads(text[len(MULTI_REPLY_MARKER) :])
except json.JSONDecodeError:
return None
if (
not isinstance(payload, list)
or not payload
or any(not isinstance(item, str) or not item.strip() for item in payload)
):
return None
return [item.strip() for item in payload]
def local_command_parts(question: str) -> tuple[str, str]:
normalized = question.lstrip()
if not normalized.startswith("/"):
@@ -131,7 +162,11 @@ class LocalCommandEngine:
}:
return self.reply_cleaner(self.players.handle(row, kind, arguments))
if kind == "cs_autochess":
return self.reply_cleaner(self.autochess.handle(row))
parts = [
self.reply_cleaner(part)
for part in self.autochess.handle_parts(row)
]
return pack_reply_parts(parts)
if kind == "dice":
return self.reply_cleaner(self.dice(row, arguments))
if kind == "dice_challenge":
@@ -14,6 +14,8 @@ from .local_commands import (
LocalCommandEngine,
local_command_parts,
utc_now,
pack_reply_parts,
unpack_reply_parts,
)
@@ -74,7 +76,11 @@ class LocalServiceBroker:
"question": f"{command} {arguments}".rstrip(),
"created_at": str(event.get("observed_at") or utc_now()),
}
return {"text": self.engine.handle(row, event)}
text = self.engine.handle(row, event)
parts = unpack_reply_parts(text)
if parts is not None:
return {"text": parts[0], "texts": parts}
return {"text": text}
class _Context:
@@ -162,6 +168,8 @@ class LocalExtensionRuntime:
result = handler(sdk_event, arguments)
if not isinstance(result, ExtensionResult):
raise LocalExtensionError("local adapter returned invalid result")
if result.reply.kind is PlanKind.MULTI_TEXT:
return pack_reply_parts(list(result.reply.messages))
if result.reply.kind is not PlanKind.TEXT:
raise LocalExtensionError("local command returned no visible reply")
return result.reply.message
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-sdk"
version = "0.6.5a0+1015"
version = "0.6.6a0+1015"
description = "Typed, capability-limited extension API for JWeBot plugins"
requires-python = ">=3.9"
dependencies = []
@@ -38,4 +38,4 @@ __all__ = [
"SingleGroupContext",
]
__version__ = "0.6.5a0+1015"
__version__ = "0.6.6a0+1015"
@@ -20,6 +20,7 @@ class ContractViolation(ValueError):
class PlanKind(str, Enum):
TEXT = "text"
MULTI_TEXT = "multi_text"
NO_REPLY = "no_reply"
@@ -156,6 +157,7 @@ class ReplyPlan:
kind: PlanKind
message: str = ""
messages: tuple[str, ...] = ()
def __post_init__(self) -> None:
if self.kind is PlanKind.TEXT:
@@ -168,13 +170,40 @@ class ReplyPlan:
limit=MAX_REPLY_CHARS,
),
)
if self.messages:
raise ContractViolation("text plan cannot contain multiple messages")
elif self.kind is PlanKind.MULTI_TEXT:
if self.message:
raise ContractViolation("multi_text plan cannot contain text")
if not self.messages:
raise ContractViolation("multi_text plan requires messages")
if len(self.messages) > 8:
raise ContractViolation("multi_text plan exceeds 8 messages")
object.__setattr__(
self,
"messages",
tuple(
_required_text(
message,
name="reply message",
limit=MAX_REPLY_CHARS,
)
for message in self.messages
),
)
elif self.message:
raise ContractViolation("no_reply plan cannot contain text")
elif self.messages:
raise ContractViolation("no_reply plan cannot contain messages")
@classmethod
def text(cls, message: str) -> "ReplyPlan":
return cls(kind=PlanKind.TEXT, message=message)
@classmethod
def multi_text(cls, messages: tuple[str, ...] | list[str]) -> "ReplyPlan":
return cls(kind=PlanKind.MULTI_TEXT, messages=tuple(messages))
@classmethod
def no_reply(cls) -> "ReplyPlan":
return cls(kind=PlanKind.NO_REPLY)
@@ -189,14 +218,23 @@ class ReplyPlan:
raise ContractViolation("reply plan kind is invalid") from error
if kind is PlanKind.TEXT:
return cls.text(str(value.get("text", "")))
if kind is PlanKind.MULTI_TEXT:
messages = value.get("texts", ())
if not isinstance(messages, list):
raise ContractViolation("multi_text plan messages must be a list")
return cls.multi_text([str(message) for message in messages])
if value.get("text"):
raise ContractViolation("no_reply plan cannot contain text")
if value.get("texts"):
raise ContractViolation("no_reply plan cannot contain messages")
return cls.no_reply()
def to_mapping(self) -> dict[str, str]:
def to_mapping(self) -> dict[str, Any]:
value = {"kind": self.kind.value}
if self.kind is PlanKind.TEXT:
value["text"] = self.message
if self.kind is PlanKind.MULTI_TEXT:
value["texts"] = list(self.messages)
return value