Release beta 2.0.0.1021
check / local-gate (push) Failing after 1m38s

This commit is contained in:
JWeBot Contributors
2026-10-21 17:05:09 +08:00
parent 5a3c4fb94c
commit e4365841ed
79 changed files with 1572 additions and 1840 deletions
+154 -30
View File
@@ -116,6 +116,30 @@ class RouterInvocationUnavailable(RuntimeError):
"""A transient route invocation failure that must be retried by the queue."""
def load_conversation_shadow_support() -> tuple[object | None, Callable[..., Any] | None]:
if not os.environ.get("JWEBOT_CONVERSATION_SHADOW_CONFIG", "").strip():
return None, None
root = Path(__file__).resolve().parent
source_roots = [
root / "packages" / package / "src"
for package in (
"jwebot-config",
"jwebot-sdk",
"jwebot-official-extensions",
"jwebot-kernel",
)
]
for source_root in reversed(source_roots):
if source_root.is_dir():
sys.path.insert(0, str(source_root))
from jwebot_kernel.conversation_scope import ( # noqa: PLC0415
ShadowConversationRegistry,
build_shadow_event,
)
return ShadowConversationRegistry.from_environment(), build_shadow_event
def router_error_message(
*,
result: subprocess.CompletedProcess[str] | None = None,
@@ -1577,27 +1601,81 @@ class Recorder:
self.state["last_visual_hash"] = ""
self.state["visible_signatures"] = []
self.state["recent_trigger_signatures"] = []
raw_conversations = self.state.get("conversation_states")
if not isinstance(raw_conversations, dict):
raw_conversations = {}
self.state["conversation_states"] = {
str(fingerprint): value
for fingerprint, value in raw_conversations.items()
if isinstance(value, dict)
and value.get("message_signature_scheme") == MESSAGE_SIGNATURE_SCHEME
}
self.pending_hash = ""
self.pending_count = 0
self.pending_frames: dict[str, tuple[str, int]] = {}
self.no_overlap_recovery_ready = False
self.recovery_ready_conversations: set[str] = set()
(
self.shadow_conversations,
self.build_shadow_event,
) = load_conversation_shadow_support()
def save_state(self) -> None:
write_json(RECORDER_STATE, self.state)
def conversation_state(self, fingerprint: str) -> dict[str, Any]:
conversations = self.state["conversation_states"]
value = conversations.get(fingerprint)
if isinstance(value, dict):
return value
if (
fingerprint == self.state.get("last_group_fingerprint")
and self.state.get("message_signature_scheme")
== MESSAGE_SIGNATURE_SCHEME
):
value = {
"initialized": bool(self.state.get("initialized")),
"last_layout_hash": str(self.state.get("last_layout_hash", "")),
"last_visual_hash": str(self.state.get("last_visual_hash", "")),
"visible_signatures": list(
self.state.get("visible_signatures", [])
),
"recent_trigger_signatures": list(
self.state.get("recent_trigger_signatures", [])
),
"message_signature_scheme": MESSAGE_SIGNATURE_SCHEME,
}
else:
value = {
"initialized": False,
"last_layout_hash": "",
"last_visual_hash": "",
"visible_signatures": [],
"recent_trigger_signatures": [],
"message_signature_scheme": MESSAGE_SIGNATURE_SCHEME,
}
conversations[fingerprint] = value
return value
def capture_cycle(self, *, stable_frames: int) -> None:
started = time.monotonic()
image = capture_image()
crop = message_image(image)
layout_hash, visual_hash = incoming_frame_hashes(crop)
if visual_hash != self.pending_hash:
self.pending_hash = visual_hash
self.pending_count = 1
group = title_fingerprint(image)
conversation = self.conversation_state(group)
pending_hash, pending_count = self.pending_frames.get(group, ("", 0))
if visual_hash != pending_hash:
pending_count = 1
else:
self.pending_count += 1
pending_count += 1
self.pending_frames[group] = (visual_hash, pending_count)
self.pending_hash = visual_hash
self.pending_count = pending_count
outcome = "pending"
if (
self.pending_count >= stable_frames
and visual_hash != self.state.get("last_visual_hash")
pending_count >= stable_frames
and visual_hash != conversation.get("last_visual_hash")
):
outcome = "process"
self.process_stable_image(
@@ -1605,12 +1683,13 @@ class Recorder:
layout_hash,
visual_hash=visual_hash,
observed_at=now_iso(),
group_fingerprint=group,
)
telemetry_event(
"ocr.capture",
elapsed_ms=(time.monotonic() - started) * 1000,
outcome=outcome,
count=self.pending_count,
count=pending_count,
source="screen",
)
@@ -1634,60 +1713,89 @@ class Recorder:
*,
visual_hash: str | None = None,
observed_at: str | None = None,
group_fingerprint: str | None = None,
) -> None:
pipeline_started = time.monotonic()
crop = message_image(image)
_, bubbles = incoming_mask_and_bubbles(crop)
messages, elapsed = self.engine.recognize_messages(crop, bubbles)
group = group_fingerprint or title_fingerprint(image)
conversation = self.conversation_state(group)
shadow_target = (
self.shadow_conversations.target_for_fingerprint(group)
if self.shadow_conversations is not None
else None
)
previous = [
value
for value in self.state.get("visible_signatures", [])
for value in conversation.get("visible_signatures", [])
if isinstance(value, str)
]
recent_trigger_signatures = [
value
for value in self.state.get("recent_trigger_signatures", [])
for value in conversation.get("recent_trigger_signatures", [])
if isinstance(value, str)
]
new_messages, current_signatures, overlap, resynced = new_sequence_suffix(
previous,
messages,
recent_trigger_signatures=recent_trigger_signatures,
recover_unseen_trigger=self.no_overlap_recovery_ready,
recover_unseen_trigger=group in self.recovery_ready_conversations,
)
initialized = bool(self.state.get("initialized"))
initialized = bool(conversation.get("initialized"))
observed_at = observed_at or now_iso()
if not initialized:
new_messages = []
resynced = True
self.state["initialized"] = True
group = title_fingerprint(image)
conversation["initialized"] = True
current_trigger_signatures = [
message.signature
for message in messages
if trigger_question(message.text) is not None
if (
shadow_target.question(message.text, bot_mention=BOT_MENTION)
if shadow_target is not None
else trigger_question(message.text)
)
]
recent_trigger_signatures = (
recent_trigger_signatures + current_trigger_signatures
)[-MAX_RECENT_TRIGGER_SIGNATURES:]
self.recovery_ready_conversations.add(group)
self.no_overlap_recovery_ready = True
appended = 0
queued = 0
shadowed = 0
for ordinal, message in enumerate(new_messages):
event = history_event(
message,
observed_at=observed_at,
group_fingerprint=group,
ordinal=ordinal,
)
if (
self.queue_enabled
and event["mention_bot"]
and message.sender != "unknown"
and message.sender_confidence >= MIN_SENDER_CONFIDENCE
):
queue_event(event)
queued += 1
if shadow_target is not None:
if self.build_shadow_event is None:
raise RuntimeError("shadow event builder is unavailable")
event = self.build_shadow_event(
shadow_target,
observed_at=observed_at,
text=message.text,
body_confidence=message.body_confidence,
visual_signature=message.signature,
bot_mention=BOT_MENTION,
observed_sender=message.sender,
sender_confidence=message.sender_confidence,
ordinal=ordinal,
)
shadowed += 1
else:
event = history_event(
message,
observed_at=observed_at,
group_fingerprint=group,
ordinal=ordinal,
)
if (
self.queue_enabled
and event["mention_bot"]
and message.sender != "unknown"
and message.sender_confidence >= MIN_SENDER_CONFIDENCE
):
queue_event(event)
queued += 1
write_history_event(event)
appended += 1
logging.info(
@@ -1698,9 +1806,23 @@ class Recorder:
message.confidence,
bool(queued),
)
conversation.update(
{
"last_capture_at": observed_at,
"last_layout_hash": layout_hash,
"last_visual_hash": visual_hash or layout_hash,
"last_ocr_ms": round(elapsed * 1000),
"visible_signatures": current_signatures,
"recent_trigger_signatures": recent_trigger_signatures,
"message_signature_scheme": MESSAGE_SIGNATURE_SCHEME,
"visible_count": len(messages),
"last_overlap": overlap,
"last_resynced": resynced,
}
)
self.state.update(
{
"version": 1,
"version": 2,
"last_capture_at": observed_at,
"last_layout_hash": layout_hash,
"last_visual_hash": visual_hash or layout_hash,
@@ -1714,6 +1836,8 @@ class Recorder:
"last_resynced": resynced,
"events_total": int(self.state.get("events_total", 0)) + appended,
"queued_total": int(self.state.get("queued_total", 0)) + queued,
"shadowed_total": int(self.state.get("shadowed_total", 0))
+ shadowed,
"resync_total": int(self.state.get("resync_total", 0))
+ int(resynced and initialized),
"errors_total": int(self.state.get("errors_total", 0)),
@@ -1747,7 +1871,7 @@ class Recorder:
group_id=group,
count=appended,
depth=queued,
outcome="queued" if queued else "observed",
outcome="shadowed" if shadowed else "queued" if queued else "observed",
source="rapidocr",
)