Skip to content

Commit 0b84aa4

Browse files
scszcoderclaude
andcommitted
ws050: 转人工 handover emoji-ack + synthetic-card placeholder 弹不出 fix
Two fixes from the 1v8 (ws045) trace. 1) 转人工 handover acknowledgement (the customer-flagged requirement). When a customer transfers to human, Feige auto-greets ("Hi,欢迎光临本店…") and starts a service-attitude penalty timer. That greeting is the PLATFORM's — it does NOT count as our reply — so we must send our own. We now send a uniform ASCII ":)" once per handover: - frontdesk_dispatch: when first_system_row_match returns a handover-family reason (store_auto_greeting / smart_cs_auto_greeting / human_handover_notice / store_assignment_notice / transfer_to_human_label), record the customer via placeholder_timer.note_handover_ack_needed (deduped 600s, since the greeting row re-matches every cycle until the customer types). - placeholder_timer: the sweep loop drains pending acks and sends the emoji through the SAME submitter placeholders use (open conversation + type), inheriting browser_session/worker_loop/tab-pool — no new send plumbing. Gated ECAN_FEIGE_HANDOVER_ACK (default ON; =0 disables). Detection is DOM/ dispatch-based because the WS fusion-handover frame (switch_human_triggered_word=人工, verified by decoding the 06-10 capture) carries no usable talk_id — only shop-side fusion ids. 2) Synthetic-card placeholder 弹不出. A placeholder armed under 'card:<conv>' failed feige_open_session with "Session not found" (live 1v8: 6x for card:7650132942676575524 — 半成品男孩's card conv) because the sidebar row is the real name, not 'card:<conv>'. direct_delivery now de-synthesizes the conv to the real name (ws_session.name_for_talk) before open AND send, falling back to the synthetic key if unresolved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 671566e commit 0b84aa4

3 files changed

Lines changed: 122 additions & 2 deletions

File tree

agent/ec_skills/browser_use_extension/hooks/external/feige_chat/direct_delivery.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,23 @@ async def _ph_invoke(action, params):
317317
raise
318318
return _raw_call
319319

320-
_open_params = _open_fn.param_model(customer_name=customer_key)
320+
# ws050: a placeholder armed under the synthetic 'card:<conv>' identity
321+
# fails feige_open_session with "Session not found" — the sidebar row is
322+
# the customer's REAL name, not 'card:<conv>' (live 1v8: 6x for
323+
# card:7650132942676575524). De-synthesize to the real name before
324+
# opening so the placeholder actually delivers (弹不出 fix). Falls back to
325+
# the synthetic key if the WS stream never resolved a name for the conv.
326+
_open_name = customer_key
327+
if str(customer_key).startswith("card:"):
328+
_conv = str(customer_key)[len("card:"):]
329+
try:
330+
from . import ws_session as _wss_open
331+
_real = str(_wss_open.name_for_talk(_conv) or "").strip()
332+
if _real and not _real.startswith("card:"):
333+
_open_name = _real
334+
except Exception:
335+
pass
336+
_open_params = _open_fn.param_model(customer_name=_open_name)
321337
await _ph_invoke(_open_fn, _open_params)
322338

323339
# 2026-05-20 v3: re-check suppression AFTER feige_open_session
@@ -344,7 +360,7 @@ async def _ph_invoke(action, params):
344360
# specific bubble — it's a stand-by message).
345361
_send_params = _send_fn.param_model(
346362
text=text,
347-
customer_name=customer_key,
363+
customer_name=_open_name,
348364
source_customer_msg_id="",
349365
source_latest_message="",
350366
)

agent/ec_skills/browser_use_extension/hooks/external/feige_chat/placeholder_timer.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,64 @@ class _TimerEntry:
207207
_REGISTRY: dict[tuple[str, str], _TimerEntry] = {}
208208
_REGISTRY_LOCK = threading.Lock()
209209

210+
# ws050: 转人工 handover acknowledgement ──────────────────────────────
211+
# When a customer transfers to human (转人工), Feige auto-greets ("Hi,欢迎光临
212+
# 本店…") and starts a service-attitude penalty timer. That system greeting is
213+
# the PLATFORM's, not ours — it does NOT stop the clock; we must send our OWN
214+
# message. We send a uniform ASCII emoji once per handover, routed through the
215+
# placeholder send path (open conversation + type), reusing the sweeper's
216+
# browser_session/worker_loop. Gated by ECAN_FEIGE_HANDOVER_ACK (default ON;
217+
# set =0 to disable). Deduped per customer for _HANDOVER_ACK_REDEDUP_S so the
218+
# persistent greeting row (which re-matches every dispatch cycle until the
219+
# customer types) acks exactly once.
220+
_HANDOVER_ACK_TEXT = ":)"
221+
_HANDOVER_ACK_REDEDUP_S = 600.0
222+
_handover_ack_pending: dict[str, float] = {}
223+
_handover_ack_done: dict[str, float] = {}
224+
_handover_ack_lock = threading.Lock()
225+
226+
227+
def handover_ack_enabled() -> bool:
228+
return os.environ.get("ECAN_FEIGE_HANDOVER_ACK", "1") != "0"
229+
230+
231+
def note_handover_ack_needed(customer_key: str) -> None:
232+
"""Mark *customer_key* as owing a 转人工 handover emoji. Idempotent and
233+
rate-limited: a customer acked within the last _HANDOVER_ACK_REDEDUP_S is
234+
skipped (the greeting row keeps re-matching until the customer types)."""
235+
if not customer_key or not handover_ack_enabled():
236+
return
237+
now = time.time()
238+
with _handover_ack_lock:
239+
if now - _handover_ack_done.get(customer_key, 0.0) < _HANDOVER_ACK_REDEDUP_S:
240+
return
241+
_handover_ack_pending.setdefault(customer_key, now)
242+
243+
244+
def clear_handover_ack(customer_key: str) -> None:
245+
"""Re-arm: a genuine new customer message means a LATER handover for the
246+
same customer may be acked again."""
247+
if not customer_key:
248+
return
249+
with _handover_ack_lock:
250+
_handover_ack_pending.pop(customer_key, None)
251+
_handover_ack_done.pop(customer_key, None)
252+
253+
254+
def _drain_handover_acks() -> list[str]:
255+
"""Return + clear the customers currently owing a handover emoji, stamping
256+
each as done (for the re-dedup window)."""
257+
now = time.time()
258+
with _handover_ack_lock:
259+
custs = list(_handover_ack_pending.keys())
260+
for c in custs:
261+
_handover_ack_pending.pop(c, None)
262+
_handover_ack_done[c] = now
263+
for c in [c for c, t in _handover_ack_done.items()
264+
if now - t > _HANDOVER_ACK_REDEDUP_S * 2]:
265+
_handover_ack_done.pop(c, None)
266+
return custs
267+
210268
# Per-customer typed-placeholder ledger (2026-05-21 Fix B).
211269
# Defense in depth: even when multiple timers exist for the same customer
212270
# (e.g., PreDispatch mis-dispatched a phantom turn + the real turn — Fix A
@@ -1036,6 +1094,19 @@ async def sweep_loop_async(
10361094
while True:
10371095
try:
10381096
await _asyncio.sleep(interval_s)
1097+
# ws050: drain pending 转人工 handover acks — send the uniform ASCII
1098+
# emoji once per handover via the SAME submitter (open + type) that
1099+
# placeholders use, so it inherits browser_session/worker_loop/pool.
1100+
for _hk in _drain_handover_acks():
1101+
logger.info(
1102+
f"[placeholder_timer] ws050 handover-ack -> cust={_hk!r} "
1103+
f"text={_HANDOVER_ACK_TEXT!r}")
1104+
try:
1105+
placeholder_submitter(_hk, "", _HANDOVER_ACK_TEXT)
1106+
except Exception as _hae:
1107+
logger.debug(
1108+
f"[placeholder_timer] handover-ack submit failed "
1109+
f"cust={_hk!r}: {_hae}")
10391110
expired = claim_expired(
10401111
max_placeholders=max_placeholders,
10411112
rearm_s=rearm_s,

agent/ec_skills/node_runtime/frontdesk_dispatch.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@
4343

4444
logger = logging.getLogger("eCan")
4545

46+
# ws050: system_message_filter reasons that signal a 转人工 handover (customer
47+
# transferred to human + Feige's auto-greeting/接入) and so should trigger our
48+
# one-time ASCII-emoji acknowledgement. Excludes pure noise (已读/草稿/delivery).
49+
_HANDOVER_ACK_REASONS = frozenset({
50+
"store_auto_greeting",
51+
"smart_cs_auto_greeting",
52+
"human_handover_notice",
53+
"store_assignment_notice",
54+
"transfer_to_human_label",
55+
})
56+
4657

4758
# ─── mt052D Day 1 — out-of-band (OOB) parallel dispatch foundation ─────
4859
#
@@ -1248,6 +1259,28 @@ def _pick_first(item: dict, keys: list[str]) -> str:
12481259
)
12491260
system_reason = first_system_row_match(item)
12501261
if system_reason:
1262+
# ws050: a 转人工 handover / store-greeting / 接入 row means the
1263+
# customer transferred to human and Feige started its penalty
1264+
# timer. Its auto-greeting does NOT count as our reply — send a
1265+
# one-time ASCII emoji ack. Recorded here (customer in hand);
1266+
# the placeholder sweeper drains + sends it (it has browser_session).
1267+
if system_reason in _HANDOVER_ACK_REASONS:
1268+
try:
1269+
from agent.ec_skills.browser_use_extension.hooks.external.feige_chat.placeholder_timer import (
1270+
note_handover_ack_needed as _note_handover_ack,
1271+
)
1272+
_ho_cust = str(
1273+
item.get("customer_name")
1274+
or item.get("customer_id")
1275+
or item.get("session_id")
1276+
or ""
1277+
).strip()
1278+
if _ho_cust and not _ho_cust.startswith("card:"):
1279+
_note_handover_ack(_ho_cust)
1280+
except Exception as _ho_err:
1281+
logger.debug(
1282+
f"[BrowserAutomation] {cfg.log_tag} ws050 handover-ack "
1283+
f"note failed: {_ho_err}")
12511284
pending_marker = any(
12521285
str(item.get(k) or "").strip()
12531286
for k in (

0 commit comments

Comments
 (0)