From 31abad86d5f013bb20427e41b3155dc5fe0a7d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Wed, 13 May 2026 20:04:06 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspace/agents/agent-card.tsx | 80 +- .../workspace/messages/message-list.tsx | 512 +++++---- frontend-web/src/core/messages/utils.ts | 56 +- frontend-web/src/pages/NewAgentPage.tsx | 994 +++++++++--------- .../backend/app/gateway/routers/agents.py | 41 +- .../deerflow/agents/lead_agent/agent.py | 11 +- .../setup_agent_approval_middleware.py | 90 ++ .../deerflow/persistence/agents/base.py | 9 + .../deerflow/persistence/agents/memory.py | 10 + .../deerflow/persistence/agents/sql.py | 13 + .../tools/builtins/setup_agent_tool.py | 6 +- .../backend/tests/test_agents_backend.py | 57 + 12 files changed, 1127 insertions(+), 752 deletions(-) create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/setup_agent_approval_middleware.py diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index 6927b216..cc136d61 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -59,11 +59,17 @@ export function AgentCard({ agent }: AgentCardProps) { agent.skills ?? [], ); - const currentUserId = useMemo(() => getStoredAuth()?.user_id ?? null, []); + const storedAuth = useMemo(() => getStoredAuth(), []); + const currentUserId = storedAuth?.user_id ?? null; + const isAdmin = storedAuth?.system_role === "admin"; const isBuiltin = agent.builtin; const isOwned = !isBuiltin && agent.user_id !== null && agent.user_id === currentUserId; const isFromOthers = !isBuiltin && !isOwned; + // System admins are allowed to edit built-in agents in place. Delete and + // publish-toggle stay owner-only — built-ins shouldn't be removable, and + // they are inherently visible to everyone so toggling published is moot. + const canEdit = isOwned || (isBuiltin && isAdmin); useEffect(() => { if (!editOpen) { @@ -235,40 +241,46 @@ export function AgentCard({ agent }: AgentCardProps) { {t.agents.chat} - {isOwned && ( + {(canEdit || isOwned) && (
- - - + {isOwned && ( + + )} + {canEdit && ( + + )} + {isOwned && ( + + )}
)} diff --git a/frontend-web/src/components/workspace/messages/message-list.tsx b/frontend-web/src/components/workspace/messages/message-list.tsx index b7f2d582..4a37f957 100644 --- a/frontend-web/src/components/workspace/messages/message-list.tsx +++ b/frontend-web/src/components/workspace/messages/message-list.tsx @@ -1,14 +1,14 @@ -import type { BaseStream } from "@langchain/langgraph-sdk/react"; -import { SendIcon } from "lucide-react"; -import { useMemo, useState } from "react"; - -import { - Conversation, - ConversationContent, -} from "@/components/ai-elements/conversation"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { useI18n } from "@/core/i18n/hooks"; +import type { BaseStream } from "@langchain/langgraph-sdk/react"; +import { CheckIcon, SendIcon, XIcon } from "lucide-react"; +import { useMemo, useState } from "react"; + +import { + Conversation, + ConversationContent, +} from "@/components/ai-elements/conversation"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { useI18n } from "@/core/i18n/hooks"; import { extractContentFromMessage, extractPresentFilesFromMessage, @@ -42,17 +42,21 @@ export function MessageList({ className, threadId, thread, - paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM, - tokenUsageEnabled = false, - onClarificationSubmit, -}: { - className?: string; - threadId: string; - thread: BaseStream; - paddingBottom?: number; - tokenUsageEnabled?: boolean; - onClarificationSubmit?: (answer: string) => void | Promise; -}) { + paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM, + tokenUsageEnabled = false, + onClarificationSubmit, + onSetupAgentApprove, + onSetupAgentReject, +}: { + className?: string; + threadId: string; + thread: BaseStream; + paddingBottom?: number; + tokenUsageEnabled?: boolean; + onClarificationSubmit?: (answer: string) => void | Promise; + onSetupAgentApprove?: (payload: SetupAgentApprovalPayload) => void | Promise; + onSetupAgentReject?: (payload: SetupAgentApprovalPayload) => void | Promise; +}) { const { t } = useI18n(); const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading); const updateSubtask = useUpdateSubtask(); @@ -83,13 +87,13 @@ export function MessageList({ if (message && hasContent(message)) { return (
- + + + +
+ ) : null; } else if (group.type === "assistant:present-files") { const files: string[] = []; for (const message of group.messages) { @@ -246,150 +267,289 @@ export function MessageList({
- ); -} - -type ClarificationOption = { - id: string; - label: string; - description?: string; -}; - -type ClarificationPayload = { - question?: string; - context?: string | null; - clarification_type?: string; - options?: ClarificationOption[]; - allow_custom?: boolean; -}; - -function getClarificationPayload(message: unknown): ClarificationPayload | null { - if (typeof message !== "object" || message === null) { - return null; - } - const additionalKwargs = Reflect.get(message, "additional_kwargs"); - if (typeof additionalKwargs !== "object" || additionalKwargs === null) { - return null; - } - const clarification = Reflect.get(additionalKwargs, "clarification"); - if (typeof clarification !== "object" || clarification === null) { - return null; - } - return clarification as ClarificationPayload; -} - -function ClarificationCard({ - fallbackContent, - isLoading, - message, - onSubmit, - rehypePlugins, -}: { - fallbackContent: string; - isLoading: boolean; - message: unknown; - onSubmit?: (answer: string) => void | Promise; - rehypePlugins: ReturnType; -}) { - const payload = getClarificationPayload(message); - const [customAnswer, setCustomAnswer] = useState(""); - const [submitting, setSubmitting] = useState(false); - - const options = useMemo( - () => - (payload?.options ?? []).filter( - (option) => typeof option.label === "string" && option.label.trim(), - ), - [payload?.options], - ); - - if (!payload) { - return ( - - ); - } - - const disabled = isLoading || submitting || !onSubmit; - - async function submitAnswer(answer: string) { - const trimmed = answer.trim(); - if (!trimmed || disabled) { - return; - } - setSubmitting(true); - try { - await onSubmit?.(trimmed); - } finally { - setSubmitting(false); - } - } - - return ( -
-
-
- 需要你的确认 -
- {payload.context ? ( -

- {payload.context} -

- ) : null} -
- {payload.question || fallbackContent} -
-
- - {options.length > 0 ? ( -
- {options.map((option) => ( - - ))} -
- ) : null} - - {payload.allow_custom !== false ? ( -
-