From f590f672be595e582a6f98531b0e10e4f496ac50 Mon Sep 17 00:00:00 2001 From: Administrator Date: Tue, 2 Jun 2026 23:48:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(skills/settings):=20=E6=8A=80=E8=83=BD?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=20+=20=E8=AE=BE=E7=BD=AE=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=E7=AD=89=E4=BC=9A=E8=AF=9D=E5=89=8D=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 技能复制:新增 POST 复制接口(_rewrite_skill_frontmatter_name 重写 frontmatter name + 校验 + 去重),前端 skills api/hooks 与技能设置页配套 - 设置持久化:lastAgentId、线程 RAG 模式锁(settings/index、local) - i18n(en/zh/types)、agent-card/input-box/message-list 等页面文案与交互微调 - scheduler/element.py 小修 (本批为会话前已存在的工作区改动,按要求一并提交;构建产物 backend.zip 未提交) Co-Authored-By: Claude Opus 4.8 --- .../workspace/agents/agent-card.tsx | 57 +++++- .../src/components/workspace/input-box.tsx | 2 +- .../workspace/messages/message-list-item.tsx | 169 +++++++++++++++--- .../workspace/messages/message-list.tsx | 3 + .../settings/skill-settings-page.tsx | 78 ++++++++ frontend-web/src/core/i18n/locales/en-US.ts | 2 + frontend-web/src/core/i18n/locales/types.ts | 2 + frontend-web/src/core/i18n/locales/zh-CN.ts | 2 + frontend-web/src/core/settings/index.ts | 7 +- frontend-web/src/core/settings/local.ts | 24 +++ frontend-web/src/core/skills/api.ts | 18 ++ frontend-web/src/core/skills/hooks.ts | 17 ++ frontend-web/src/pages/AgentChatPage.tsx | 6 +- frontend-web/src/pages/ChatPage.tsx | 36 +++- frontend-web/src/pages/NewAgentPage.tsx | 6 +- .../roundtable-planning/lib/clarification.ts | 14 +- .../backend/app/gateway/routers/skills.py | 123 ++++++++++++- .../deerflow/runtime/scheduler/element.py | 2 +- 18 files changed, 524 insertions(+), 44 deletions(-) diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index a7e2b79b..a3002755 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -2,6 +2,7 @@ import { BotIcon, + CopyIcon, GlobeIcon, InfoIcon, LockIcon, @@ -43,12 +44,14 @@ import { TagChipList } from "@/components/workspace/tags/tag-chip"; import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog"; import { useAgent, + useCreateAgent, useDeleteAgent, useSetAgentFeatured, useSetAgentPrivate, useToggleAgentFavorite, useUpdateAgent, } from "@/core/agents"; +import { getAgent } from "@/core/agents/api"; import type { Agent } from "@/core/agents"; import { getStoredAuth } from "@/core/auth"; import { useI18n } from "@/core/i18n/hooks"; @@ -65,6 +68,7 @@ export function AgentCard({ agent }: AgentCardProps) { const router = useRouter(); const deleteAgent = useDeleteAgent(); const updateAgent = useUpdateAgent(); + const createAgent = useCreateAgent(); const setFeatured = useSetAgentFeatured(); const setPrivate = useSetAgentPrivate(); const toggleFavorite = useToggleAgentFavorite(); @@ -153,6 +157,41 @@ export function AgentCard({ agent }: AgentCardProps) { } } + async function handleDuplicate() { + if (createAgent.isPending) { + return; + } + try { + // SOUL.md is omitted from the list payload (perf), so fetch the detail + // row to carry the source agent's personality into the copy. Fall back + // to whatever the list happened to include if the detail fetch fails. + let soul = agent.soul ?? ""; + try { + const detail = await getAgent(agent.id); + soul = detail.soul ?? soul; + } catch { + // keep the list fallback + } + const created = await createAgent.mutateAsync({ + name: `${agent.name} 副本`, + description: agent.description ?? "", + soul, + skills: agent.skills ?? [], + model: agent.model ?? null, + tool_groups: agent.tool_groups ?? null, + // A copy starts as a private, unpublished draft the owner can refine + // before sharing — never inherit the source's published/private state. + published: false, + is_private: false, + }); + toast.success( + `已复制为「${desensitize(created.name)}」,可在「我的」中编辑`, + ); + } catch (err) { + toast.error(err instanceof Error ? err.message : String(err)); + } + } + async function handleSaveEdit() { const trimmedName = editName.trim(); if (!trimmedName) { @@ -471,6 +510,16 @@ export function AgentCard({ agent }: AgentCardProps) { )} + {canEdit && ( diff --git a/frontend-web/src/components/workspace/input-box.tsx b/frontend-web/src/components/workspace/input-box.tsx index f1739d53..da17a139 100644 --- a/frontend-web/src/components/workspace/input-box.tsx +++ b/frontend-web/src/components/workspace/input-box.tsx @@ -583,7 +583,7 @@ export function InputBox({ // 对话态:常规白卡 + 中性边框 + 轻阴影 isNewThread ? "rounded-3xl bg-transparent border-0 shadow-none ring-0" - : "rounded-3xl bg-background border border-border shadow-sm dark:bg-slate-800 dark:border-slate-700", + : "rounded-3xl bg-background border border-border shadow-sm transition-shadow hover:shadow-md focus-within:border-ring/40 focus-within:shadow-md focus-within:ring-1 focus-within:ring-ring/20 dark:bg-slate-800 dark:border-slate-700", className, )} disabled={disabled} diff --git a/frontend-web/src/components/workspace/messages/message-list-item.tsx b/frontend-web/src/components/workspace/messages/message-list-item.tsx index f7d551ac..d997d415 100644 --- a/frontend-web/src/components/workspace/messages/message-list-item.tsx +++ b/frontend-web/src/components/workspace/messages/message-list-item.tsx @@ -1,9 +1,10 @@ import type { Message } from "@langchain/langgraph-sdk"; -import { ClockIcon, FileIcon, Loader2Icon } from "lucide-react"; +import { ClockIcon, FileIcon, Loader2Icon, PencilIcon } from "lucide-react"; import { memo, useCallback, useContext, + useEffect, useMemo, useState, type AnchorHTMLAttributes, @@ -25,6 +26,8 @@ import { } from "@/components/ai-elements/reasoning"; import { Task, TaskTrigger } from "@/components/ai-elements/task"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; import { UploadedFilePreviewDialog, useAuthedObjectUrl, @@ -51,6 +54,7 @@ import { cn } from "@/lib/utils"; import { CitationLink } from "../citations/citation-link"; import { CopyButton } from "../copy-button"; +import { Tooltip } from "../tooltip"; import { SkillDisplayBlock } from "../skill-display-block"; import { ThreadContext } from "./context"; import { DownloadWordButton } from "./download-word-button"; @@ -88,6 +92,7 @@ export function MessageListItem({ tokenUsageEnabled = false, displaySources = [], extraRehypePlugins, + onEditResubmit, }: { className?: string; message: Message; @@ -96,6 +101,7 @@ export function MessageListItem({ tokenUsageEnabled?: boolean; displaySources?: RuntimeDisplaySource[]; extraRehypePlugins?: ReturnType; + onEditResubmit?: (text: string) => void; }) { const isHuman = message.type === "human"; const messageMarkdown = @@ -115,17 +121,23 @@ export function MessageListItem({ tokenUsageEnabled={tokenUsageEnabled} displaySources={displaySources} extraRehypePlugins={extraRehypePlugins} + onEditResubmit={onEditResubmit} /> - {!isLoading && ( + {/* + Human messages carry their own inline copy/edit toolbar inside + MessageContent (above the timestamp). Only assistant answers use the + floating bottom toolbar here. + */} + {!isLoading && !isHuman && (
- {!isHuman && messageMarkdown && ( + {messageMarkdown && ( void; +}) { + const { t } = useI18n(); + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(text); + + useEffect(() => { + if (!editing) { + setDraft(text); + } + }, [text, editing]); + + const confirmEdit = useCallback(() => { + const value = draft.trim(); + if (!value) { + return; + } + onEditResubmit?.(value); + setEditing(false); + }, [draft, onEditResubmit]); + + if (editing) { + return ( +
+ {filesList} +