From a41b5a6d4ce776852c925c356a8ebc4aac72a22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Fri, 5 Jun 2026 21:45:21 +0800 Subject: [PATCH] fix: improve agent handoff and leaderboard scheduling --- .../workspace/skills/skill-detail-dialog.tsx | 15 ++---- frontend-web/src/pages/AgentChatPage.tsx | 11 ++-- frontend-web/src/pages/ChatPage.tsx | 51 +++++++++++++++---- .../app/gateway/routers/admin_users.py | 24 +++++++++ 4 files changed, 75 insertions(+), 26 deletions(-) diff --git a/frontend-web/src/components/workspace/skills/skill-detail-dialog.tsx b/frontend-web/src/components/workspace/skills/skill-detail-dialog.tsx index 3dbe16ce..94148451 100644 --- a/frontend-web/src/components/workspace/skills/skill-detail-dialog.tsx +++ b/frontend-web/src/components/workspace/skills/skill-detail-dialog.tsx @@ -133,8 +133,7 @@ export function SkillDetailDialog({ return stripYamlFrontmatter(contentData?.content ?? ""); }, [contentData]); - const defaultTab: "detail" | "source" = - skill && !skill.detail ? "source" : "detail"; + const defaultTab: "detail" = "detail"; const selectedTab = activeTab ?? defaultTab; function startEditing() { @@ -234,10 +233,6 @@ export function SkillDetailDialog({ {tx.filesSectionTitle} )} - - - {tx.contentSectionTitle} -
@@ -337,13 +332,9 @@ export function SkillDetailDialog({

{tx.detailEmpty}

- {isOwner ? ( -

- Open the SKILL.md tab to view the skill source. -

- ) : ( + {isOwner && canManage ? (

{tx.detailAdminOnlyHint}

- )} + ) : null}
)} diff --git a/frontend-web/src/pages/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index 061c2ef1..3a834120 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -131,8 +131,8 @@ export default function AgentChatPage() { const pendingMessageHandled = useRef(false); useEffect(() => { if (pendingMessageHandled.current) return; - if (!agent_id || !isNewThread) return; - let pending: { agentId?: string; text?: string } | null = null; + if (!agent_id) return; + let pending: { agentId?: string; threadId?: string; text?: string } | null = null; try { const raw = sessionStorage.getItem("pendingAgentMessage"); if (raw) pending = JSON.parse(raw); @@ -140,10 +140,15 @@ export default function AgentChatPage() { pending = null; } if (!pending || pending.agentId !== agent_id || !pending.text) return; + if (pending.threadId) { + if (pending.threadId !== threadId) return; + } else if (!isNewThread) { + return; + } pendingMessageHandled.current = true; sessionStorage.removeItem("pendingAgentMessage"); handleSubmit({ text: pending.text, files: [] }); - }, [agent_id, isNewThread, handleSubmit]); + }, [agent_id, threadId, isNewThread, handleSubmit]); const handleClarificationSubmit = useCallback( (answer: string) => { diff --git a/frontend-web/src/pages/ChatPage.tsx b/frontend-web/src/pages/ChatPage.tsx index fd3f34cd..f96db7f6 100644 --- a/frontend-web/src/pages/ChatPage.tsx +++ b/frontend-web/src/pages/ChatPage.tsx @@ -1,9 +1,11 @@ "use client"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; import { type PromptInputMessage } from "@/components/ai-elements/prompt-input"; +import { getAPIClient } from "@/core/api"; import { usePromptPrefixSettings } from "@/core/system-settings/hooks"; import { useUserPreferencesQuery } from "@/core/user-preferences"; import { ArtifactTrigger, useArtifacts } from "@/components/workspace/artifacts"; @@ -58,6 +60,8 @@ export default function ChatPage() { const hideAdminPrompt = userPreferences?.hide_admin_prompt_prefix === true; const [promptPrefixOn, setPromptPrefixOn] = useState(false); const [selectedAgentId, setSelectedAgentId] = useState(null); + const [agentHandoffPending, setAgentHandoffPending] = useState(false); + const agentHandoffPendingRef = useRef(false); const { agents: selectorAgents, isLoading: selectorLoading } = useAgentSelector(); const { selectedArtifact } = useArtifacts(); @@ -148,15 +152,37 @@ export default function ChatPage() { message.text && (!message.files || message.files.length === 0) ) { - try { - sessionStorage.setItem( - "pendingAgentMessage", - JSON.stringify({ agentId: selectedAgentId, text: message.text }), - ); - } catch { - // sessionStorage may be unavailable in private mode; fall through. - } - router.push(`/page/workspace/agents/${selectedAgentId}/chats/new`); + if (agentHandoffPendingRef.current) return; + const text = message.text.trim(); + agentHandoffPendingRef.current = true; + setAgentHandoffPending(true); + setHasStartedConversation(true); + void (async () => { + try { + const created = await getAPIClient(isMock).threads.create({ + metadata: { agent_id: selectedAgentId }, + }); + sessionStorage.setItem( + "pendingAgentMessage", + JSON.stringify({ + agentId: selectedAgentId, + threadId: created.thread_id, + text, + }), + ); + saveLastAgentId(selectedAgentId); + router.push( + `/page/workspace/agents/${selectedAgentId}/chats/${created.thread_id}`, + ); + } catch (err) { + agentHandoffPendingRef.current = false; + setAgentHandoffPending(false); + setHasStartedConversation(false); + toast.error( + err instanceof Error ? err.message : "创建智能体对话失败", + ); + } + })(); return; } @@ -200,6 +226,8 @@ export default function ChatPage() { hideAdminPrompt, preferencesLoading, selectedAgentId, + agentHandoffPending, + isMock, router, settings.context.writing_mode, writingArtifactPath, @@ -345,7 +373,8 @@ export default function ChatPage() { context={settings.context} disabled={ env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || - isUploading + isUploading || + agentHandoffPending } onContextChange={(context) => setSettings("context", context) diff --git a/offline-backend-20260512/backend/app/gateway/routers/admin_users.py b/offline-backend-20260512/backend/app/gateway/routers/admin_users.py index 829361d2..516b5ab8 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/admin_users.py +++ b/offline-backend-20260512/backend/app/gateway/routers/admin_users.py @@ -72,6 +72,14 @@ _LEADERBOARD_CACHE_TTL_SECONDS = max( 5.0, float(os.getenv("ADMIN_LEADERBOARD_CACHE_TTL_SECONDS", "300")), ) +_LEADERBOARD_QUERY_PAUSE_SECONDS = max( + 0.0, + float(os.getenv("ADMIN_LEADERBOARD_QUERY_PAUSE_SECONDS", "0.05")), +) +_LEADERBOARD_START_DELAY_SECONDS = max( + 0.0, + float(os.getenv("ADMIN_LEADERBOARD_START_DELAY_SECONDS", "0.2")), +) _leaderboard_semaphore = asyncio.Semaphore(_LEADERBOARD_MAX_CONCURRENT) _leaderboard_jobs: dict[str, asyncio.Task] = {} _leaderboard_cache: dict[str, dict] = {} @@ -1035,6 +1043,11 @@ def _leaderboard_cache_key( return sha256(json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest() +async def _leaderboard_pause() -> None: + if _LEADERBOARD_QUERY_PAUSE_SECONDS > 0: + await asyncio.sleep(_LEADERBOARD_QUERY_PAUSE_SECONDS) + + async def _build_user_leaderboard_response( session_factory, settings, @@ -1064,16 +1077,25 @@ async def _build_user_leaderboard_response( "cleanup_system_questions": settings.cleanup_system_questions, } overview = await _query_overview(session, since_dt, until_dt, **common) + await _leaderboard_pause() users = await _query_user_leaderboard(session, since_dt, until_dt, **common) + await _leaderboard_pause() skills = await _query_call_rankings(session, since_dt, until_dt, user_id=None, field="skill", limit=limit, **tool_common) + await _leaderboard_pause() tools = await _query_call_rankings(session, since_dt, until_dt, user_id=None, field="tool", limit=limit, **tool_common) + await _leaderboard_pause() models = await _query_model_rankings(session, since_dt, until_dt, user_id=None, limit=limit, **common) if not models: models = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="model", limit=limit, **common) + await _leaderboard_pause() agents = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="agent", limit=limit, **common) + await _leaderboard_pause() run_statuses = await _query_run_dimensions(session, since_dt, until_dt, user_id=None, field="status", limit=limit, **common) + await _leaderboard_pause() recent_questions = await _query_recent_questions(session, since_dt, until_dt, user_id=None, limit=limit, **question_common) + await _leaderboard_pause() frequent_questions = await _query_frequent_questions(session, since_dt, until_dt, limit=limit, **question_common) + await _leaderboard_pause() trends = await _query_trends(session, since_dt, until_dt, **common) return AdminLeaderboardResponse( @@ -1113,6 +1135,8 @@ async def _refresh_leaderboard_cache( ) -> None: acquired = False try: + if _LEADERBOARD_START_DELAY_SECONDS > 0: + await asyncio.sleep(_LEADERBOARD_START_DELAY_SECONDS) await _leaderboard_semaphore.acquire() acquired = True response = await asyncio.wait_for(