feat(skills/settings): 技能复制 + 设置持久化等会话前改动

- 技能复制:新增 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 <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-02 23:48:19 +08:00
1 parent d9afaca51f
commit f590f672be
18 files changed
+524 -44

No files matched your search

@@ -7,6 +7,7 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
ClockIcon,
CopyIcon,
FileArchive,
GlobeIcon,
InfoIcon,
@@ -50,6 +51,7 @@ import {
ItemContent,
ItemDescription,
} from "@/components/ui/item";
import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
import { TagChipList } from "@/components/workspace/tags/tag-chip";
@@ -61,6 +63,7 @@ import { useI18n } from "@/core/i18n/hooks";
import type { ValidateSkillUploadResponse } from "@/core/skills/api";
import {
useDeleteCustomSkill,
useDuplicateSkill,
useInstallSkillUpload,
useReconcileCustomSkills,
useSetSkillPinned,
@@ -390,9 +393,12 @@ export function SkillRow({
const setPublished = useSetSkillPublished();
const setPinned = useSetSkillPinned();
const deleteSkill = useDeleteCustomSkill();
const duplicateSkillMutation = useDuplicateSkill();
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [tagPickerOpen, setTagPickerOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
const [duplicateOpen, setDuplicateOpen] = useState(false);
const [duplicateName, setDuplicateName] = useState("");
const isCustom = skill.category === "custom";
// Built-in flag falls back to "no DB owner" so legacy responses without
@@ -428,6 +434,31 @@ export function SkillRow({
}
}
function handleOpenDuplicate() {
// Suggest a "<name>-copy" default that already satisfies the backend's
// lowercase/digit/-/_ skill-name rule. The user can edit it before saving.
setDuplicateName(`${skill.name}-copy`);
setDuplicateOpen(true);
}
async function handleDuplicate() {
const target = duplicateName.trim();
if (!target) {
toast.error("请输入新技能名称");
return;
}
try {
const result = await duplicateSkillMutation.mutateAsync({
sourceName: skill.name,
newName: target,
});
toast.success(`已复制为「${result.skill_name}」`);
setDuplicateOpen(false);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handleTogglePinned() {
const next = !skill.pinned;
try {
@@ -587,6 +618,16 @@ export function SkillRow({
)}
</Button>
)}
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
disabled={duplicateSkillMutation.isPending}
onClick={handleOpenDuplicate}
title="复制为新技能"
>
<CopyIcon className="size-3.5" />
</Button>
{canManage && (
<Button
size="icon"
@@ -600,6 +641,43 @@ export function SkillRow({
)}
</ItemActions>
</Item>
<Dialog open={duplicateOpen} onOpenChange={setDuplicateOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>复制技能</DialogTitle>
<DialogDescription>
将「{desensitize(skill.name)}」复制为一个属于你的新技能。请输入新名称(仅小写字母、数字、- 或 _)。
</DialogDescription>
</DialogHeader>
<Input
value={duplicateName}
onChange={(e) => setDuplicateName(e.currentTarget.value)}
placeholder="new-skill-name"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void handleDuplicate();
}
}}
/>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDuplicateOpen(false)}
disabled={duplicateSkillMutation.isPending}
>
{t.common.cancel}
</Button>
<Button
onClick={() => void handleDuplicate()}
disabled={duplicateSkillMutation.isPending}
>
{duplicateSkillMutation.isPending ? t.common.loading : "复制"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={confirmDeleteOpen} onOpenChange={setConfirmDeleteOpen}>
<DialogContent>
<DialogHeader>