Skip to content

Commit 883b9b4

Browse files
Merge pull request #190 from scszcoder/dev
Dev
2 parents 5d8f0a1 + 9bd3c54 commit 883b9b4

20 files changed

Lines changed: 607 additions & 151 deletions

File tree

agent/cloud_api/appsync_schema.graphql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,9 @@ type WanChatMessage @aws_api_key @aws_cognito_user_pools {
600600
chatID: String
601601
sender: String
602602
receiver: String
603+
type: String
604+
contents: String
605+
parameters: String
603606
msg: String
604607
options: AWSJSON
605608
background: String

agent/cloud_api/cloud_api.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2230,8 +2230,15 @@ def safe_parse_response(jresp, operation_name, data_key):
22302230
return response_data
22312231
else:
22322232
# No data and errors - this is a failure
2233-
logger.error(f"❌ GraphQL Error: {error_message}")
2234-
logger.error(f"📋 Full error response: {json.dumps(jresp, ensure_ascii=False)}")
2233+
# Detect "Cannot return null for non-nullable type" as a known backend schema issue
2234+
is_schema_null_error = "Cannot return null for non-nullable type" in error_message
2235+
if is_schema_null_error:
2236+
logger.error(f"❌ GraphQL Schema Error: {error_message}")
2237+
logger.error(f"📋 This is a known backend issue: AppSync resolver returned null for a non-nullable field.")
2238+
logger.error(f"📋 Action required: Check AppSync Lambda resolver for '{operation_name}' - ensure it returns all required fields.")
2239+
else:
2240+
logger.error(f"❌ GraphQL Error: {error_message}")
2241+
logger.error(f"📋 Full error response: {json.dumps(jresp, ensure_ascii=False)}")
22352242
raise Exception(f"{operation_name} failed: {error_message}")
22362243
else:
22372244
if response_data is not None:
@@ -2252,6 +2259,7 @@ def safe_parse_response(jresp, operation_name, data_key):
22522259
logger.debug(f" 3. Data validation failed on server")
22532260
logger.debug(f" 4. Permission denied (check IAM/Cognito)")
22542261
logger.debug(f" 5. Backend timeout or internal error")
2262+
return None
22552263

22562264
# =================================================================================================
22572265
# interface appsync, directly use HTTP request.

agent/ec_skills/browser_use_extension/deepseek_adapter.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
"""
1717

1818
import json
19-
import logging
20-
import re
2119
from typing import Any, Dict, List, Optional
2220

2321
from utils.logger_helper import logger_helper as logger
@@ -31,11 +29,14 @@ class DeepSeekOutputAdapter:
3129
adapter = DeepSeekOutputAdapter()
3230
"""
3331

34-
# Valid browser-use action types
32+
# Valid browser-use action types (current version)
3533
VALID_ACTIONS = {
36-
'go_to_url', 'click', 'input_text', 'scroll', 'extract',
37-
'done', 'search_google', 'open_tab', 'go_back', 'switch_tab',
38-
'close_tab', 'save_file', 'get_dropdown_options', 'select_dropdown_option'
34+
'navigate', 'click', 'input', 'scroll', 'extract',
35+
'done', 'search', 'go_back', 'wait', 'switch', 'close',
36+
'send_keys', 'find_text', 'upload_file', 'dropdown_options', 'select_dropdown',
37+
# Legacy names (still accepted for filtering, will be remapped elsewhere)
38+
'go_to_url', 'input_text', 'search_google', 'open_tab', 'switch_tab',
39+
'close_tab', 'click_element', 'extract_content', 'scroll_down', 'scroll_up',
3940
}
4041

4142
# Invalid actions that DeepSeek sometimes generates

agent/ec_skills/browser_use_extension/qwen_adapter.py

Lines changed: 252 additions & 34 deletions
Large diffs are not rendered by default.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
Monkey-patch browser_use BrowserSession to increase navigation timeout.
3+
4+
browser_use hardcodes page readiness timeout at 4s (cross-domain) / 2s (same-domain)
5+
in BrowserSession._navigate_and_wait. This is too short for many real-world sites
6+
(e.g. baidu.com, amazon.com) that have heavy async loading.
7+
8+
This patch increases the defaults to reduce "Page readiness timeout" warnings.
9+
The patch is idempotent — calling it multiple times is safe.
10+
"""
11+
12+
13+
from utils.logger_helper import logger_helper as logger
14+
15+
_patched = False
16+
17+
18+
def patch_navigation_timeout(cross_domain_timeout: float = 10.0, same_domain_timeout: float = 5.0):
19+
"""
20+
Patch BrowserSession._navigate_and_wait to use larger default timeouts.
21+
22+
Args:
23+
cross_domain_timeout: Timeout for cross-domain navigation (default: 10s, was 4s)
24+
same_domain_timeout: Timeout for same-domain navigation (default: 5s, was 2s)
25+
"""
26+
global _patched
27+
if _patched:
28+
return
29+
30+
try:
31+
from browser_use.browser.session import BrowserSession
32+
import asyncio
33+
34+
original_navigate_and_wait = BrowserSession._navigate_and_wait
35+
36+
async def _patched_navigate_and_wait(self, url: str, target_id: str, timeout: float | None = None):
37+
"""Patched version with increased default timeouts."""
38+
if timeout is None:
39+
# Use our increased timeouts instead of the hardcoded 4s/2s
40+
try:
41+
target = self.session_manager.get_target(target_id)
42+
current_url = target.url
43+
same_domain = (
44+
url.split('/')[2] == current_url.split('/')[2]
45+
if url.startswith('http') and current_url.startswith('http')
46+
else False
47+
)
48+
timeout = same_domain_timeout if same_domain else cross_domain_timeout
49+
except Exception:
50+
timeout = cross_domain_timeout
51+
52+
return await original_navigate_and_wait(self, url, target_id, timeout=timeout)
53+
54+
BrowserSession._navigate_and_wait = _patched_navigate_and_wait
55+
_patched = True
56+
logger.info(f"[SessionPatch] ✅ Patched navigation timeout: cross-domain={cross_domain_timeout}s, same-domain={same_domain_timeout}s")
57+
58+
except Exception as e:
59+
logger.warning(f"[SessionPatch] ⚠️ Failed to patch navigation timeout: {e}")

agent/ec_skills/build_node.py

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ def build_llm_node(config_metadata: dict, node_name, skill_name, owner, bp_manag
644644
hard_timeout_config = str(hard_timeout_val).lower() in ('true', '1', 'yes', 'on') if hard_timeout_val else False
645645
except Exception:
646646
pass
647-
# Prefer explicit provider; infer from apiHost if absent
647+
# Get explicit provider from frontend (guaranteed by form-meta.tsx)
648648
raw_provider = None
649649
try:
650650
raw_provider = ((inputs.get("modelProvider") or {}).get("content")
@@ -705,28 +705,13 @@ def build_llm_node(config_metadata: dict, node_name, skill_name, owner, bp_manag
705705
user_prompt_template = get_prompt_content(user_prompt_id, resolved_user_prompt)
706706
else:
707707
user_prompt_template = resolved_user_prompt
708-
# Infer provider when not explicitly set
709-
def _infer_provider(host: str, model: str) -> str:
710-
try:
711-
h = (host or "").lower()
712-
m = (model or "").lower()
713-
if "anthropic" in h or m.startswith("claude"):
714-
return "anthropic"
715-
if "google" in h or "generativeai" in h or m.startswith("gemini"):
716-
return "google"
717-
return "openai"
718-
except Exception:
719-
return "openai"
720-
721-
model_provider = raw_provider or _infer_provider(api_host, model_name)
722-
llm_provider = (model_provider or "openai").lower()
723-
724708
# Normalize provider names dynamically from llm_manager
725709
# This automatically syncs with gui/config/llm_providers.json
726710
def _get_provider_mapping() -> dict:
727711
"""
728712
Dynamically build provider mapping from llm_manager.
729-
This ensures consistency with llm_providers.json and reduces maintenance.
713+
Maps all known name variants (name, display_name, class_name, provider_id)
714+
to the canonical provider_id. No hardcoded provider list needed.
730715
731716
Returns:
732717
Dictionary mapping various provider name formats to canonical provider identifiers
@@ -774,18 +759,17 @@ def _get_provider_mapping() -> dict:
774759
return {}
775760

776761
# Get dynamic provider mapping from llm_manager
777-
# This automatically includes all providers defined in llm_providers.json:
778-
# - provider_id (e.g., "deepseek")
779-
# - name (e.g., "DeepSeek")
780-
# - display_name
781-
# - class_name (e.g., "ChatDeepSeek")
762+
# Resolves any name variant (name/display_name/class_name/provider_id) → canonical provider_id
782763
provider_mapping = _get_provider_mapping()
764+
765+
# Resolve raw_provider (e.g. "DeepSeek", "Qwen (DashScope)") to canonical provider_id (e.g. "deepseek", "dashscope")
766+
# Frontend is responsible for always passing correct modelProvider via form-meta.tsx
767+
model_provider = provider_mapping.get((raw_provider or "").lower(), raw_provider or "openai")
768+
llm_provider = (model_provider or "openai").lower()
783769

784770
logger.info(f"llm config: system_prompt_template='{system_prompt_template}' user_prompt_template='{user_prompt_template}' ")
785771
logger.info(f"llm config: model_name={model_name} api_host={api_host} api_key={api_key} model_provider={model_provider} llm_provider={llm_provider}")
786772

787-
llm_provider = provider_mapping.get(llm_provider, llm_provider)
788-
789773
# This is the actual function that will be executed as the node in the graph
790774
def llm_node_callable(state: dict, runtime=None, store=None, **kwargs) -> dict:
791775
"""
@@ -3262,6 +3246,11 @@ async def _run_browser_use(task: str, mainwin, state: dict | None = None, callin
32623246
from browser_use.browser.profile import BrowserProfile
32633247
from agent.ec_skills.browser_use_extension.extension_tools_service import custom_controller
32643248
# from browser_use.browser.context import BUBrowserContext as BUBrowserContext
3249+
3250+
# Patch navigation timeout to reduce "Page readiness timeout" warnings
3251+
# browser_use hardcodes 4s cross-domain / 2s same-domain, which is too short for many sites
3252+
from agent.ec_skills.browser_use_extension.session_patch import patch_navigation_timeout
3253+
patch_navigation_timeout(cross_domain_timeout=10.0, same_domain_timeout=5.0)
32653254
log_msg = f"🤖 Executing node Browser Automation node: {node_name}"
32663255
logger.debug(log_msg)
32673256
send_skill_editor_log("log", log_msg)
@@ -3491,9 +3480,22 @@ async def _run_browser_use(task: str, mainwin, state: dict | None = None, callin
34913480
from config.app_settings import app_settings
34923481
disable_extensions = app_settings.is_dev_mode
34933482

3494-
# Create browser profile with extensions control
3483+
# Create browser profile with extensions control and persistent user_data_dir
34953484
# Note: BrowserProfile uses enable_default_extensions (not disable_extensions)
3496-
browser_profile = BrowserProfile(enable_default_extensions=not disable_extensions)
3485+
# Auto-assign persistent user_data_dir so login state survives restarts
3486+
profile_settings = _get_browser_profile_settings(node_profile)
3487+
_bp_user_data_dir = profile_settings.get('user_data_dir', '') if profile_settings else ''
3488+
if not _bp_user_data_dir:
3489+
from utils.user_path_helper import ensure_user_data_dir
3490+
import re as _re
3491+
_bp_id = profile_settings.get('id') or profile_settings.get('name') or node_profile or 'default'
3492+
_bp_safe_id = _re.sub(r'[^\w\-]', '_', str(_bp_id))
3493+
_bp_user_data_dir = ensure_user_data_dir(subdir=os.path.join('browser_profiles', _bp_safe_id))
3494+
logger.info(f"[BrowserAutomation] Auto-assigned user_data_dir: {_bp_user_data_dir}")
3495+
browser_profile = BrowserProfile(
3496+
enable_default_extensions=not disable_extensions,
3497+
user_data_dir=_bp_user_data_dir,
3498+
)
34973499
logger.info(f"[BrowserAutomation] Extensions {'disabled (dev mode)' if disable_extensions else 'enabled (production mode)'}")
34983500

34993501
if browser_profile:

agent/ec_skills/llm_utils/llm_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2071,7 +2071,8 @@ def create_browser_use_llm(mainwin=None, fallback_llm=None, skip_playwright_chec
20712071
# Get supports_vision from config (default True if not found)
20722072
supports_vision = config.get('supports_vision', True)
20732073

2074-
log_msg = f"[create_browser_use_llm] provider_type:{provider_type}, model_name:{model_name}, api_key:{api_key}, base_url:{base_url}, class_name:{class_name}, supports_vision:{supports_vision}"
2074+
masked_key = (api_key[:8] + '...' + api_key[-4:]) if api_key and len(api_key) > 12 else '***'
2075+
log_msg = f"[create_browser_use_llm] provider_type:{provider_type}, model_name:{model_name}, api_key:{masked_key}, base_url:{base_url}, class_name:{class_name}, supports_vision:{supports_vision}"
20752076
logger.debug(log_msg)
20762077
send_skill_editor_log("log", log_msg)
20772078

auth/cognito/cognito_service.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,22 +120,43 @@ def confirm_sign_up(self, username, confirmation_code):
120120

121121
def login(self, username, password):
122122
import time
123+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
123124
start_time = time.time()
124125

126+
# Overall timeout to prevent indefinite waiting (e.g., DNS/SSL delays beyond boto3's control)
127+
# boto3 retries internally (max_attempts * (connect_timeout + read_timeout) ≈ 105s worst case)
128+
# We add a hard ceiling of 60s to fail fast for users in high-latency networks
129+
auth_flow_config = perf_config.get_auth_flow_config()
130+
overall_timeout = auth_flow_config.get('total_timeout', 30)
131+
125132
try:
126133
if perf_config.should_log_timing():
127134
logger.info(f"Starting Cognito authentication: {username}")
128135

129136
client = self._get_cognito_client()
130137

131-
response = client.initiate_auth(
132-
ClientId=AuthConfig.COGNITO.CLIENT_ID,
133-
AuthFlow='USER_PASSWORD_AUTH',
134-
AuthParameters={
135-
'USERNAME': username,
136-
'PASSWORD': password
137-
}
138-
)
138+
def _do_auth():
139+
return client.initiate_auth(
140+
ClientId=AuthConfig.COGNITO.CLIENT_ID,
141+
AuthFlow='USER_PASSWORD_AUTH',
142+
AuthParameters={
143+
'USERNAME': username,
144+
'PASSWORD': password
145+
}
146+
)
147+
148+
# Execute with overall timeout to prevent 185s+ waits
149+
with ThreadPoolExecutor(max_workers=1) as executor:
150+
future = executor.submit(_do_auth)
151+
try:
152+
response = future.result(timeout=overall_timeout)
153+
except FuturesTimeoutError:
154+
elapsed_time = time.time() - start_time
155+
logger.error(f"Cognito authentication overall timeout: {username}, "
156+
f"timeout={overall_timeout}s, elapsed: {elapsed_time:.2f}s")
157+
# Reset cognito_client so next attempt creates a fresh connection
158+
self.cognito_client = None
159+
return {'success': False, 'error': f'Authentication timeout after {overall_timeout}s. Please check your network connection.'}
139160

140161
elapsed_time = time.time() - start_time
141162

auth/performance_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class AuthPerformanceConfig:
2121

2222
# Authentication flow configuration
2323
'auth_flow': {
24-
'total_timeout': 30, # Total authentication timeout (seconds) - increased for stability
24+
'total_timeout': 45, # Total authentication timeout (seconds) - hard ceiling for entire auth flow
2525
},
2626

2727
# Performance monitoring configuration

0 commit comments

Comments
 (0)