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(