Skip to content

Commit dd6f94b

Browse files
committed
fix under non-ideal site.
1 parent 856a232 commit dd6f94b

9 files changed

Lines changed: 186 additions & 14 deletions

File tree

agent/ec_skills/browser_use_extension/event_monitor.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2293,6 +2293,23 @@ def _is_nonempty(value: Any) -> bool:
22932293
_dropped_reasons = []
22942294
for _item in added_items:
22952295
_reason = _feige_first_system_row_match(_item)
2296+
if _reason and isinstance(_item, dict):
2297+
_pending_marker = any(
2298+
str(_item.get(k) or "").strip()
2299+
for k in (
2300+
"pending_timer",
2301+
"unread_badge",
2302+
"unread",
2303+
"needs_action",
2304+
)
2305+
)
2306+
if _pending_marker:
2307+
logger.info(
2308+
f"[EventMonitor] Feige filter keeping pending "
2309+
f"system-looking row for thread enrichment: "
2310+
f"reason={_reason!r} customer={_item.get('customer_name')!r}"
2311+
)
2312+
_reason = ""
22962313
if not _reason and isinstance(_item, dict):
22972314
_cust_key = _feige_normalize_customer_key(
22982315
str(

agent/ec_skills/browser_use_extension/extension_tools_service.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3318,6 +3318,10 @@ async def feige_get_chat_thread(params: FeigeGetChatThreadAction, browser_sessio
33183318
}
33193319
function latestCustomerBubble() {
33203320
var wrappers = Array.from(document.querySelectorAll('[data-qa-id="qa-message-warpper"]'));
3321+
function isTransferMarker(text) {
3322+
var t = String(text || '').replace(/\s+/g, '').trim();
3323+
return t === '转人工' || t === '转人工客服' || t === '人工客服';
3324+
}
33213325
for (var i = wrappers.length - 1; i >= 0; i--) {
33223326
var wrap = wrappers[i];
33233327
var row = wrap.querySelector('.Ie29C7uLyEjZzd8JeS8A');
@@ -3344,6 +3348,7 @@ async def feige_get_chat_thread(params: FeigeGetChatThreadAction, browser_sessio
33443348
}
33453349
}
33463350
if (!text && !hasContentImage) continue;
3351+
if (text && isTransferMarker(text)) continue;
33473352
var idEl = wrap.querySelector('[data-id]');
33483353
return {
33493354
found: true,

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,10 @@ def _normalize_reply_text(text: str) -> str:
517517
}
518518
return atts;
519519
}
520+
function _isTransferMarker(text) {
521+
var t = String(text || '').replace(/\s+/g, '').trim();
522+
return t === '转人工' || t === '转人工客服' || t === '人工客服';
523+
}
520524
var wrappers = Array.from(document.querySelectorAll('[data-qa-id="qa-message-warpper"]'));
521525
for (var i = wrappers.length - 1; i >= 0; i--) {
522526
var wrap = wrappers[i];
@@ -533,6 +537,7 @@ def _normalize_reply_text(text: str) -> str:
533537
// a content image. Image-only bubbles (text === '') were silently
534538
// dropped before this change.
535539
if (!text && attachments.length === 0) continue;
540+
if (text && _isTransferMarker(text)) continue;
536541
// ── Merge attachments from immediately-prior customer bubbles ──
537542
// Real-world multimodal chats fire as bursts: e.g. (image, text)
538543
// or (text, image, text). When the latest bubble we picked is
@@ -1435,6 +1440,41 @@ async def scrape_latest_customer_bubble(
14351440
break
14361441
if _attempt == 0:
14371442
await _s_asyncio.sleep(0.25)
1443+
if not verify_ok:
1444+
logger.info(
1445+
f"[BrowserAutomation] scrape-latest-customer: active-customer "
1446+
f"verification mismatch after sidebar click for {customer_name!r}; "
1447+
"retrying click once"
1448+
)
1449+
retry_raw = await _s_eval_js(browser_session, _click_js)
1450+
if isinstance(retry_raw, str):
1451+
try:
1452+
retry_data = json.loads(retry_raw)
1453+
except Exception:
1454+
retry_data = {}
1455+
else:
1456+
retry_data = retry_raw if isinstance(retry_raw, dict) else {}
1457+
if retry_data.get("ok"):
1458+
if not retry_data.get("already_active"):
1459+
await _s_asyncio.sleep(0.5)
1460+
for _attempt in range(3):
1461+
verify_raw = await _s_eval_js(
1462+
browser_session, FEIGE_ACTIVE_CUSTOMER_JS
1463+
)
1464+
if isinstance(verify_raw, str):
1465+
try:
1466+
verify_data = json.loads(verify_raw)
1467+
except Exception:
1468+
verify_data = {}
1469+
else:
1470+
verify_data = verify_raw if isinstance(verify_raw, dict) else {}
1471+
verify_ok, verify_reason = verify_customer_match(
1472+
verify_data, customer_name
1473+
)
1474+
if verify_ok:
1475+
break
1476+
if _attempt < 2:
1477+
await _s_asyncio.sleep(0.25)
14381478
if not verify_ok:
14391479
logger.warning(
14401480
f"[BrowserAutomation] scrape-latest-customer: active-customer "

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import inspect
6969
import json
7070
import logging
71+
import os
7172
from dataclasses import dataclass, field
7273
from typing import Any, Callable
7374

@@ -95,7 +96,13 @@
9596
PRE_SEND_REVERIFY_ATTEMPTS: int = 16
9697
PRE_SEND_REVERIFY_INTERVAL_S: float = 0.075
9798
POST_SEND_TAB_RESTORE_SLEEP_S: float = 0.3
98-
HOT_PATH_TOOL_TIMEOUT_S: float = 4.0
99+
try:
100+
HOT_PATH_TOOL_TIMEOUT_S: float = max(
101+
1.0,
102+
float(os.getenv("ECAN_HOT_PATH_TOOL_TIMEOUT_S", "8.0")),
103+
)
104+
except Exception:
105+
HOT_PATH_TOOL_TIMEOUT_S = 8.0
99106

100107

101108
@dataclass

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import asyncio
5050
import json
5151
import logging
52+
import os
5253
from dataclasses import dataclass, field
5354
from typing import Any, Callable, Protocol, runtime_checkable
5455

@@ -86,7 +87,13 @@
8687
PRE_SEND_REVERIFY_ATTEMPTS: int = 16
8788
PRE_SEND_REVERIFY_INTERVAL_S: float = 0.075
8889
POST_SEND_TAB_RESTORE_SLEEP_S: float = 0.3
89-
HOT_PATH_TOOL_TIMEOUT_S: float = 4.0
90+
try:
91+
HOT_PATH_TOOL_TIMEOUT_S: float = max(
92+
1.0,
93+
float(os.getenv("ECAN_HOT_PATH_TOOL_TIMEOUT_S", "8.0")),
94+
)
95+
except Exception:
96+
HOT_PATH_TOOL_TIMEOUT_S = 8.0
9097
ACTIVE_CUSTOMER_EVAL_TIMEOUT_S: float = 0.75
9198
SOURCE_TURN_EVAL_TIMEOUT_S: float = 1.0
9299

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,26 @@ async def enrich_item(
457457
)
458458
_row_hit = _first_row_match(item)
459459
if _row_hit:
460+
_pending_marker = any(
461+
str(item.get(k) or "").strip()
462+
for k in (
463+
"pending_timer",
464+
"unread_badge",
465+
"unread",
466+
"needs_action",
467+
)
468+
)
469+
if typing_lock_sidebar_only and _pending_marker:
470+
logger.info(
471+
f"[BrowserAutomation] {log_tag} system-looking pending "
472+
f"row deferred while typing lock is active for "
473+
f"cust={customer_key!r} reason={_row_hit!r}"
474+
)
475+
return EnrichResult(
476+
skip=True,
477+
skip_reason="typing_lock_active",
478+
scraped_msg_id="",
479+
)
460480
logger.info(
461481
f"[BrowserAutomation] {log_tag} system-message filter "
462482
f"SKIP for cust={customer_key!r} reason={_row_hit!r}"

agent/ec_skills/build_node.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ def _normalize_dispatch_identity_key(raw_id: str) -> str:
240240
def _stale_input_has_undelivered_response_text(
241241
stale_input: Any,
242242
new_event_type: str,
243+
previous_hot_path_type: str = "",
243244
) -> tuple[bool, str, str]:
244245
"""Detect if a soon-to-be-cleared ``state["input"]`` carries a Q&A
245246
worker reply that HOT-PATH-B has not yet typed into Feige.
@@ -273,6 +274,14 @@ def _stale_input_has_undelivered_response_text(
273274
"""
274275
if new_event_type == "chat_message":
275276
return False, "", ""
277+
if previous_hot_path_type in {
278+
"action_failed",
279+
"configurable",
280+
"dedup_skip",
281+
"stale_reply_drop",
282+
"system_reply_drop",
283+
}:
284+
return False, "", ""
276285
if not stale_input:
277286
return False, "", ""
278287
try:
@@ -7215,10 +7224,19 @@ def _deep_merge(a: dict, b: dict) -> dict:
72157224
# non-chat_message resume here would silently drop the reply.
72167225
# Restore it so HOT-PATH-B's ``state["input"]`` payload-source
72177226
# fallback can pick it up on the next BA invocation.
7227+
_prev_hot_path_type = ""
7228+
if isinstance(state.get("result"), dict):
7229+
_prev_llm_result = state["result"].get("llm_result")
7230+
if isinstance(_prev_llm_result, dict):
7231+
_prev_hot_path_type = str(
7232+
_prev_llm_result.get("hot_path_type") or ""
7233+
)
72187234
try:
72197235
_preserve, _undeliv_cust, _undeliv_resp = (
72207236
_stale_input_has_undelivered_response_text(
7221-
_stale_input, _rp_event_type
7237+
_stale_input,
7238+
_rp_event_type,
7239+
previous_hot_path_type=_prev_hot_path_type,
72227240
)
72237241
)
72247242
if _preserve:

agent/ec_skills/node_runtime/frontdesk_dispatch.py

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,10 @@ def _build_deferred_result(
323323
*,
324324
reason: str,
325325
detail: str = "",
326+
retry: bool = False,
326327
) -> dict:
327328
payload = {
328-
"all_done": True,
329+
"all_done": not retry,
329330
"work_done": False,
330331
"hot_path": True,
331332
"hot_path_type": "pre_dispatch_deferred",
@@ -337,6 +338,8 @@ def _build_deferred_result(
337338
}
338339
if detail:
339340
payload["detail"] = detail
341+
if retry:
342+
payload["retry_pending"] = True
340343
return {
341344
"final": json.dumps(payload, ensure_ascii=False),
342345
"history": f"{cfg.history_prefix}:deferred:{reason}",
@@ -505,6 +508,14 @@ def _pick_first(item: dict, keys: list[str]) -> str:
505508
# duplicate worker invocations and racing replies.
506509
"identity_key": str(item.get("identity_key") or ""),
507510
}
511+
for pending_key in (
512+
"pending_timer",
513+
"unread_badge",
514+
"unread",
515+
"needs_action",
516+
):
517+
if pending_key in item:
518+
entry[pending_key] = item.get(pending_key)
508519
for extra_key in cfg.assignment_extra_fields:
509520
ek = str(extra_key)
510521
if ek and ek not in entry and ek in item:
@@ -908,7 +919,7 @@ def _release_inflight_on_early_exit(reason: str) -> None:
908919
if enrich.skip:
909920
skip_reason = enrich.skip_reason or "unspecified"
910921
_release_inflight_on_early_exit(f"enrich_skip:{skip_reason}")
911-
if skip_reason == "typing_lock_active":
922+
if skip_reason in {"typing_lock_active", "active_customer_mismatch"}:
912923
return "", "", _TYPING_LOCK_ACTIVE_SENTINEL
913924
return opened_row, "", ""
914925
scraped_msg_id = enrich.scraped_msg_id
@@ -1025,19 +1036,21 @@ def _build_result_payload(
10251036
opened_rows: list[str],
10261037
assigned_rows: list[str],
10271038
failure_rows: list[str],
1039+
deferred_rows: list[str] | None = None,
10281040
) -> dict:
10291041
"""Final result dict returned to the caller when dispatch ran
10301042
(even partially). Matches the shape of the legacy inline
10311043
``_maybe_run_frontdesk_dispatch_fastpath`` return.
10321044
"""
1033-
return {
1034-
"all_done": True,
1045+
deferred_rows = deferred_rows or []
1046+
payload = {
1047+
"all_done": not bool(deferred_rows),
10351048
"work_result": {
10361049
cfg.fastpath_marker: True,
10371050
"visible_session_count": len(actionable),
10381051
"opened_count": len(opened_rows),
10391052
"assigned_count": len(assigned_rows),
1040-
"last_action_succeeded": not bool(failure_rows),
1053+
"last_action_succeeded": not bool(failure_rows or deferred_rows),
10411054
"no_customers": False,
10421055
},
10431056
"opened_sessions": opened_rows,
@@ -1049,6 +1062,28 @@ def _build_result_payload(
10491062
else f"{cfg.log_tag} completed with failures"
10501063
),
10511064
}
1065+
if deferred_rows:
1066+
payload["work_done"] = False
1067+
payload["hot_path"] = True
1068+
payload["hot_path_type"] = "pre_dispatch_deferred"
1069+
payload["reason"] = "feige_focus_contention"
1070+
payload["retry_pending"] = True
1071+
payload["deferred_sessions"] = deferred_rows
1072+
payload["work_result"]["deferred_count"] = len(deferred_rows)
1073+
payload["message"] = (
1074+
f"{cfg.log_tag} partially completed; "
1075+
f"{len(deferred_rows)} row(s) deferred"
1076+
)
1077+
return payload
1078+
1079+
1080+
def _deferred_row_label(item: dict) -> str:
1081+
return str(
1082+
item.get("session_id")
1083+
or item.get("customer_name")
1084+
or item.get("customer_id")
1085+
or _TYPING_LOCK_ACTIVE_SENTINEL
1086+
)
10521087

10531088

10541089
# ───────────────────────────── orchestrator ───────────────────────────────
@@ -1256,6 +1291,7 @@ async def _run_with_lock_held(
12561291
opened_rows: list[str] = []
12571292
assigned_rows: list[str] = []
12581293
failure_rows: list[str] = []
1294+
deferred_rows: list[str] = []
12591295
for item in actionable:
12601296
opened, assigned, failure = await _dispatch_one_item(
12611297
item,
@@ -1272,23 +1308,45 @@ async def _run_with_lock_held(
12721308
if assigned:
12731309
assigned_rows.append(assigned)
12741310
if failure:
1275-
failure_rows.append(failure)
1311+
if failure == _TYPING_LOCK_ACTIVE_SENTINEL:
1312+
deferred_rows.append(_deferred_row_label(item))
1313+
else:
1314+
failure_rows.append(failure)
12761315

12771316
if not opened_rows and not assigned_rows and not failure_rows:
1317+
if deferred_rows:
1318+
return _build_deferred_result(
1319+
cfg,
1320+
reason="feige_focus_contention",
1321+
detail=f"{len(deferred_rows)} row(s) deferred",
1322+
retry=True,
1323+
)
12781324
return None
12791325

1280-
payload = _build_result_payload(cfg, actionable, opened_rows, assigned_rows, failure_rows)
1326+
payload = _build_result_payload(
1327+
cfg,
1328+
actionable,
1329+
opened_rows,
1330+
assigned_rows,
1331+
failure_rows,
1332+
deferred_rows,
1333+
)
12811334
logger.info(
12821335
f"[BrowserAutomation] {cfg.log_tag} completed: "
12831336
f"visible={len(actionable)} opened={len(opened_rows)} "
1284-
f"assigned={len(assigned_rows)} failures={len(failure_rows)}"
1337+
f"assigned={len(assigned_rows)} failures={len(failure_rows)} "
1338+
f"deferred={len(deferred_rows)}"
12851339
)
12861340
# Signal any concurrently-running LLM invocation to stop — it would
12871341
# just duplicate work.
12881342
assigned_sessions = dispatch_state.setdefault("assigned_sessions", {})
1289-
if assigned_rows or (not failure_rows and assigned_sessions):
1343+
if not deferred_rows and (assigned_rows or (not failure_rows and assigned_sessions)):
12901344
setattr(session, cfg.flag_attr, True)
12911345
return {
12921346
"final": json.dumps(payload, ensure_ascii=False),
1293-
"history": cfg.history_prefix,
1347+
"history": (
1348+
f"{cfg.history_prefix}:deferred:feige_focus_contention"
1349+
if deferred_rows
1350+
else cfg.history_prefix
1351+
),
12941352
}

mcp_tools_schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"generated_at": "2026-05-07T00:08:34.478751",
2+
"generated_at": "2026-05-07T16:58:33.055493",
33
"total_tools": 132,
44
"tools": [
55
{

0 commit comments

Comments
 (0)