release: alpha 0.6.1 local player plugins and capture reconciliation
check / local-gate (push) Failing after 3m24s

This commit is contained in:
JWeBot Contributors
2026-10-15 05:01:40 +08:00
parent 84ba2193df
commit f7fedc54e4
40 changed files with 1346 additions and 38 deletions
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "0.6.0a0+1015"
version = "0.6.1a0+1015"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==0.6.0a0+1015",
"jwebot-sdk==0.6.0a0+1015",
"jwebot-official-extensions==0.6.0a0+1015",
"jwebot-config==0.6.1a0+1015",
"jwebot-sdk==0.6.1a0+1015",
"jwebot-official-extensions==0.6.1a0+1015",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "0.6.0a0+1015"
__version__ = "0.6.1a0+1015"
@@ -941,6 +941,34 @@ class Ledger:
observed_at TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS context_history (
event_id TEXT PRIMARY KEY,
group_fingerprint TEXT NOT NULL,
display_name TEXT NOT NULL,
text TEXT NOT NULL,
observed_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS context_history_group_time
ON context_history(group_fingerprint, observed_at, event_id);
INSERT OR IGNORE INTO context_history
SELECT event_id, group_fingerprint, display_name, text, observed_at
FROM repeat_history;
CREATE TABLE IF NOT EXISTS player_accounts (
group_fingerprint TEXT NOT NULL,
person_key TEXT NOT NULL,
state_json TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (group_fingerprint, person_key)
);
CREATE TABLE IF NOT EXISTS player_actions (
group_fingerprint TEXT NOT NULL,
event_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 INDEX IF NOT EXISTS events_group_created
ON events(group_fingerprint, created_at);
CREATE INDEX IF NOT EXISTS outbox_due
@@ -2713,14 +2741,20 @@ class Ledger:
) -> bool:
content = bounded_text(text, limit=1_500)
repeat_key = repeat_text_key(content)
if not event_id or not group_fingerprint or not content:
return False
person = self.resolve_person(group_fingerprint, sender)
with self.lock:
self.connection.execute(
"INSERT OR IGNORE INTO context_history VALUES (?, ?, ?, ?, ?)",
(event_id, group_fingerprint, person_display_label(person["display_name"]),
content, observed_at),
)
if (
not event_id
or not group_fingerprint
or not repeat_key
not repeat_key
or len(repeat_key) < 2
):
return False
person = self.resolve_person(group_fingerprint, sender)
with self.lock, self.connection:
cursor = self.connection.execute(
"""
@@ -2846,6 +2880,102 @@ class Ledger:
).fetchall()
return [dict(row) for row in rows]
def recent_context_messages(
self, group: str, limit: int, *, exclude_event_id: str = "",
) -> list[dict[str, Any]]:
if limit not in {100, 200, 300}:
raise LocalCommandError("上下文数量只支持 100、200、300")
with self.lock:
rows = self.connection.execute(
"""
SELECT event_id, display_name, text, observed_at
FROM context_history
WHERE group_fingerprint = ? AND event_id != ?
ORDER BY observed_at DESC, event_id DESC LIMIT ?
""", (group, exclude_event_id, limit),
).fetchall()
return [dict(row) for row in reversed(rows)]
def person_directory(self, group: str) -> list[dict[str, Any]]:
self.seed_person_aliases(group)
with self.lock:
rows = self.connection.execute(
"""
SELECT canonical_name, alias_key, match_kind FROM person_aliases
WHERE group_fingerprint = ?
ORDER BY canonical_name, match_kind, alias_key
""", (group,),
).fetchall()
people: dict[str, dict[str, Any]] = {}
for row in rows:
name = str(row["canonical_name"])
person = people.setdefault(name, {
"display_name": person_display_label(name),
"person_key": person_scope_key(group, name),
"aliases": [], "prefixes": [],
})
person["prefixes" if row["match_kind"] == "prefix" else "aliases"].append(
str(row["alias_key"]))
# Include profiles with a canonical name even when no alias was configured.
for template in PERSON_ALIAS_TEMPLATES:
name = str(template["canonical_name"])
people.setdefault(name, {
"display_name": person_display_label(name),
"person_key": person_scope_key(group, name),
"aliases": [], "prefixes": [],
})
return list(people.values())
def apply_player_action(
self, group: str, sender: str, event_id: str, command: str,
action: Callable[[dict[str, Any]], str],
) -> str:
"""Commit identity-scoped funds, game state and replay response together."""
if not group or not event_id or not sender or sender == "unknown":
raise LocalCommandError("无法确认本人身份,不能操作金币账户")
person = self.resolve_person(group, sender)
person_key = str(person["person_key"])
with self.lock:
self.connection.execute("BEGIN IMMEDIATE")
try:
previous = self.connection.execute(
"""SELECT person_key, command, response_text FROM player_actions
WHERE group_fingerprint = ? AND event_id = ?""",
(group, event_id),
).fetchone()
if previous is not None:
if previous["person_key"] != person_key or previous["command"] != command:
raise LocalCommandError("同一事件不能变更账户或游戏指令")
self.connection.execute("COMMIT")
return str(previous["response_text"])
stored = self.connection.execute(
"""SELECT state_json FROM player_accounts
WHERE group_fingerprint = ? AND person_key = ?""",
(group, person_key),
).fetchone()
account = json.loads(stored["state_json"]) if stored else {
"balance": 1000, "game": None, "wins": 0, "losses": 0, "ties": 0,
}
response = action(account)
if type(account["balance"]) is not int or not 0 <= account["balance"] <= 1_000_000_000:
raise LocalCommandError("金币结算超出允许范围")
timestamp = utc_now()
self.connection.execute(
"""INSERT INTO player_accounts VALUES (?, ?, ?, ?)
ON CONFLICT(group_fingerprint, person_key) DO UPDATE
SET state_json = excluded.state_json, updated_at = excluded.updated_at""",
(group, person_key, json.dumps(account, ensure_ascii=False), timestamp),
)
self.connection.execute(
"INSERT INTO player_actions VALUES (?, ?, ?, ?, ?, ?)",
(group, event_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]