diff --git a/backend/services/a2a_agent_adapter.py b/backend/services/a2a_agent_adapter.py
index b6fddc500..36f10657e 100644
--- a/backend/services/a2a_agent_adapter.py
+++ b/backend/services/a2a_agent_adapter.py
@@ -227,7 +227,7 @@ def build_a2a_task_response(
text_content = str(message)
task["status"]["message"] = {
"role": message.get("role", "agent"),
- "parts": [{"type": "text", "text": text_content, "mediaType": _MEDIA_TYPE_TEXT}]
+ "parts": [{"text": text_content, "mediaType": _MEDIA_TYPE_TEXT}]
}
# Handle artifacts
@@ -266,9 +266,9 @@ def build_a2a_message_response(
if parts:
message_parts = parts
elif text:
- message_parts = [{"type": "text", "text": text, "mediaType": _MEDIA_TYPE_TEXT}]
+ message_parts = [{"text": text, "mediaType": _MEDIA_TYPE_TEXT}]
else:
- message_parts = [{"type": "text", "text": "", "mediaType": _MEDIA_TYPE_TEXT}]
+ message_parts = [{"text": "", "mediaType": _MEDIA_TYPE_TEXT}]
message_obj = {
"messageId": message_id,
@@ -294,8 +294,8 @@ def _content_to_artifact_parts(
return parts
if isinstance(content, dict):
if content.get("type") == "text":
- return [{"type": "text", "text": content.get("text", "")}]
- return [{"type": "text", "text": str(content)}]
+ return [{"text": content.get("text", ""), "mediaType": _MEDIA_TYPE_TEXT}]
+ return [{"text": str(content), "mediaType": _MEDIA_TYPE_TEXT}]
def _map_task_state(self, state: str) -> str:
"""Map shorthand state to TASK_STATE constant."""
@@ -343,7 +343,7 @@ def _message_to_parts_format(self, message: Any) -> Dict[str, Any]:
text = str(message)
return {
"role": role,
- "parts": [{"type": "text", "text": text}]
+ "parts": [{"text": text}]
}
def _build_artifact_update_event(
diff --git a/backend/services/a2a_server_service.py b/backend/services/a2a_server_service.py
index 2cccbe40d..4d9c5e607 100644
--- a/backend/services/a2a_server_service.py
+++ b/backend/services/a2a_server_service.py
@@ -647,7 +647,7 @@ async def handle_message_send(
return self.adapter.build_a2a_task_response(
task_id=task_id,
status="TASK_STATE_COMPLETED",
- parts=[{"type": "text", "text": accumulated_text, "mediaType": "text/plain"}] if accumulated_text else None,
+ parts=[{"text": accumulated_text, "mediaType": "text/plain"}] if accumulated_text else None,
context_id=context_id,
timestamp=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
)
@@ -879,7 +879,7 @@ def get_task(
message = result.get("message", "")
if message:
task_obj["artifacts"] = [{
- "parts": [{"type": "text", "text": str(message)}],
+ "parts": [{"text": str(message)}],
"lastChunk": True
}]
diff --git a/backend/services/user_management_service.py b/backend/services/user_management_service.py
index b994f35b1..a00f29f29 100644
--- a/backend/services/user_management_service.py
+++ b/backend/services/user_management_service.py
@@ -15,6 +15,7 @@
from utils.auth_utils import (
get_supabase_client,
+ get_supabase_admin_client,
calculate_expires_at,
get_jwt_expiry_seconds,
)
@@ -411,6 +412,24 @@ async def get_user_info(user_id: str) -> Optional[Dict[str, Any]]:
# Get user tenant relationship
user_tenant = get_user_tenant_by_user_id(user_id)
if not user_tenant:
+ # User exists in Supabase but not in local database - this is an inconsistent state.
+ # Delete the orphaned Supabase account and return None to trigger 401.
+ logging.warning(
+ f"User {user_id} not found in local database, cleaning up orphaned Supabase account"
+ )
+ try:
+ admin_client = get_supabase_admin_client()
+ if admin_client and hasattr(admin_client.auth, "admin"):
+ admin_client.auth.admin.delete_user(user_id)
+ logging.info(f"Deleted orphaned Supabase user {user_id}")
+ else:
+ logging.warning(
+ f"Could not get Supabase admin client to delete user {user_id}"
+ )
+ except Exception as delete_err:
+ logging.error(
+ f"Failed to delete orphaned Supabase user {user_id}: {str(delete_err)}"
+ )
return None
tenant_id = user_tenant["tenant_id"]
diff --git a/doc/docs/en/user-guide/agent-development.md b/doc/docs/en/user-guide/agent-development.md
index 109674273..7637cd620 100644
--- a/doc/docs/en/user-guide/agent-development.md
+++ b/doc/docs/en/user-guide/agent-development.md
@@ -55,7 +55,7 @@ Nexent supports communication with third-party agents through the A2A protocol.
If you know the Agent Card address of the target agent, you can use the URL discovery method:
-

+
1. In the External A2A Agent list, click the "Add External Agent" button
@@ -72,7 +72,7 @@ If you know the Agent Card address of the target agent, you can use the URL disc
If your agent is registered with the Nacos service discovery platform, you can use the Nacos discovery method:
-

+
1. In the External A2A Agent list, click the "Add External Agent" button
@@ -94,7 +94,7 @@ If your agent is registered with the Nacos service discovery platform, you can u
In the External A2A Agent list, you can view and manage all discovered external agents:
-

+
1. **View Agent Details**: Click on the agent card to view its complete information, including name, description, URL, capability list, etc.
diff --git a/doc/docs/zh/user-guide/agent-development.md b/doc/docs/zh/user-guide/agent-development.md
index a8cca4a33..3edf31de7 100644
--- a/doc/docs/zh/user-guide/agent-development.md
+++ b/doc/docs/zh/user-guide/agent-development.md
@@ -55,7 +55,7 @@ Nexent 支持通过 A2A 协议与第三方 Agent 进行通信。您可以通过
如果您知道目标 Agent 的 Agent Card 地址,可以使用 URL 发现方式:
-

+
1. 在外部 A2A Agent 列表中,点击"添加外部 Agent"按钮
@@ -72,7 +72,7 @@ Nexent 支持通过 A2A 协议与第三方 Agent 进行通信。您可以通过
如果您的 Agent 注册在 Nacos 服务发现平台,可以使用 Nacos 发现方式:
-

+
1. 在外部 A2A Agent 列表中,点击"添加外部 Agent"按钮
@@ -96,7 +96,7 @@ Nexent 支持通过 A2A 协议与第三方 Agent 进行通信。您可以通过
-

+
1. **查看 Agent 详情**:点击 Agent 卡片,可以查看其完整信息,包括名称、描述、URL、能力列表等
diff --git a/frontend/const/constants.ts b/frontend/const/constants.ts
index 291144f37..26952ed4a 100644
--- a/frontend/const/constants.ts
+++ b/frontend/const/constants.ts
@@ -7,7 +7,7 @@ export const languageOptions = [
export const TOKEN_REFRESH_CD = 1 * 60 * 1000;
// If the remaining lifetime of the access token is below this threshold,
// a refresh will be attempted on user activity (sliding expiration).
-export const TOKEN_REFRESH_BEFORE_EXPIRY_MS = 5 * 60 * 1000;
+export const TOKEN_REFRESH_BEFORE_EXPIRY_MS = 30 * 60 * 1000;
// Throttle interval for activity-driven refresh checks
export const MIN_ACTIVITY_CHECK_INTERVAL_MS = 30 * 1000;
diff --git a/frontend/services/api.ts b/frontend/services/api.ts
index 38958f21a..b441ff2e0 100644
--- a/frontend/services/api.ts
+++ b/frontend/services/api.ts
@@ -437,6 +437,12 @@ export const fetchWithErrorHandling = async (
throw new ApiError(errorCode, errorMessage);
}
+ // Handle HTTP 401 - trigger session expired modal for all unauthorized errors
+ if (response.status === 401) {
+ handleSessionExpired();
+ throw new ApiError(errorCode, errorMessage);
+ }
+
// Handle custom 499 error code (client closed connection)
if (response.status === 499) {
handleSessionExpired();
diff --git a/test/backend/services/test_a2a_agent_adapter.py b/test/backend/services/test_a2a_agent_adapter.py
index 28850abfb..a06d6f4c7 100644
--- a/test/backend/services/test_a2a_agent_adapter.py
+++ b/test/backend/services/test_a2a_agent_adapter.py
@@ -702,14 +702,14 @@ def test_converts_text_content_dict(self):
content = {"type": "text", "text": "Hello from content"}
result = adapter._content_to_artifact_parts(content, None)
- assert result == [{"type": "text", "text": "Hello from content"}]
+ assert result == [{"text": "Hello from content", "mediaType": "text/plain"}]
def test_converts_non_text_content_to_string(self):
"""Test converts non-dict or non-text content to string."""
adapter = A2AAgentAdapter()
result = adapter._content_to_artifact_parts("Plain string", None)
- assert result == [{"type": "text", "text": "Plain string"}]
+ assert result == [{"text": "Plain string", "mediaType": "text/plain"}]
def test_converts_non_text_dict_to_string(self):
"""Test converts dict content without text type to string."""
@@ -717,7 +717,7 @@ def test_converts_non_text_dict_to_string(self):
content = {"type": "image", "data": "base64..."}
result = adapter._content_to_artifact_parts(content, None)
- assert result == [{"type": "text", "text": str(content)}]
+ assert result == [{"text": str(content), "mediaType": "text/plain"}]
class TestMessageToPartsFormat:
@@ -744,7 +744,6 @@ def test_converts_message_with_text_content(self):
}
result = adapter._message_to_parts_format(message)
assert result["role"] == "user"
- assert result["parts"][0]["type"] == "text"
assert result["parts"][0]["text"] == "User message content"
def test_converts_message_with_non_text_content(self):
@@ -804,7 +803,6 @@ def test_response_with_text_content_dict(self):
)
assert result["task"]["status"]["message"]["role"] == "agent"
- assert result["task"]["status"]["message"]["parts"][0]["type"] == "text"
assert result["task"]["status"]["message"]["parts"][0]["text"] == "Agent response text"
assert result["task"]["status"]["message"]["parts"][0]["mediaType"] == "text/plain"
diff --git a/test/backend/services/test_user_management_service.py b/test/backend/services/test_user_management_service.py
index ac5deba80..6e9dc170d 100644
--- a/test/backend/services/test_user_management_service.py
+++ b/test/backend/services/test_user_management_service.py
@@ -1313,18 +1313,24 @@ async def test_get_user_info_success(self, mock_query_group_ids, mock_get_user_t
{"permission_type": "LEFT_NAV_MENU", "permission_subtype": "chat"}
])
+ @patch('backend.services.user_management_service.get_supabase_admin_client')
@patch('backend.services.user_management_service.get_user_tenant_by_user_id')
- async def test_get_user_info_user_not_found(self, mock_get_user_tenant):
- """Test getting user information when user doesn't exist"""
+ async def test_get_user_info_user_not_found(self, mock_get_user_tenant, mock_get_admin_client):
+ """Test getting user information when user doesn't exist - orphan cleanup is triggered"""
# Setup mocks
mock_get_user_tenant.return_value = None
+ mock_admin_client = MagicMock()
+ mock_admin_client.auth.admin.delete_user = MagicMock()
+ mock_get_admin_client.return_value = mock_admin_client
# Execute
- result = await get_user_info("nonexistent_user")
+ result = await get_user_info("orphan_user")
# Assert
assert result is None
- mock_get_user_tenant.assert_called_once_with("nonexistent_user")
+ mock_get_user_tenant.assert_called_once_with("orphan_user")
+ mock_get_admin_client.assert_called_once()
+ mock_admin_client.auth.admin.delete_user.assert_called_once_with("orphan_user")
@patch('backend.services.user_management_service.get_user_tenant_by_user_id')
@patch('backend.services.user_management_service.query_group_ids_by_user')