feat(skills): 新增技能详情字段并统一发布图标

- 技能新增 detail 字段(skills 表新增列 + Alembic 迁移),
  由管理员或技能发布者编辑,内置/公开技能仅管理员可编辑
- 新增 PUT /api/skills/{name}/detail 接口,GET/列表接口返回 detail
- 新增可复用的技能详情弹窗组件,支持 Markdown 渲染与内联编辑
- 智能体创建/编辑选技能、智能体卡片技能徽章均可查看技能详情
- 将定时任务/智能体/技能的发布图标统一为 Globe/Lock

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.7 committed 2026-05-18 10:03:37 +08:00
1 parent 3b951efd13
commit f61d053010
17 files changed
+694 -77

No files matched your search

+37 -12
View File
@@ -3,6 +3,7 @@
import {
ArrowLeftIcon,
CheckCircleIcon,
InfoIcon,
SaveIcon,
} from "lucide-react";
import { useRouter } from "next/navigation";
@@ -24,6 +25,7 @@ import {
type SetupAgentApprovalPayload,
} from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context";
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
import { getAPIClient } from "@/core/api";
import type { Agent } from "@/core/agents";
import {
@@ -73,6 +75,7 @@ export default function NewAgentPage() {
const [isSavingDraft, setIsSavingDraft] = useState(false);
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
const [isSkillSelectionTouched, setIsSkillSelectionTouched] = useState(false);
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
const [threadPrepared, setThreadPrepared] = useState(false);
const { skills, isLoading: isSkillsLoading } = useSkills();
const { models, isLoading: isModelsLoading } = useModels();
@@ -448,20 +451,34 @@ export default function NewAgentPage() {
{enabledSkills.map((skill) => {
const checked = selectedSkills.includes(skill.name);
return (
<Button
<div
key={skill.name}
type="button"
size="sm"
variant={checked ? "default" : "outline"}
onClick={() => toggleSkill(skill)}
title={skill.description}
className={cn(
"h-8 max-w-full",
!checked && "text-muted-foreground",
)}
className="inline-flex max-w-full items-center"
>
<span className="truncate">{skill.name}</span>
</Button>
<Button
type="button"
size="sm"
variant={checked ? "default" : "outline"}
onClick={() => toggleSkill(skill)}
title={skill.description}
className={cn(
"h-8 max-w-[200px] rounded-r-none",
!checked && "text-muted-foreground",
)}
>
<span className="truncate">{skill.name}</span>
</Button>
<Button
type="button"
size="icon"
variant={checked ? "default" : "outline"}
onClick={() => setDetailSkillName(skill.name)}
title={t.settings.skills.viewDetailAction}
className="h-8 w-7 rounded-l-none border-l-0"
>
<InfoIcon className="size-3.5" />
</Button>
</div>
);
})}
</div>
@@ -469,6 +486,14 @@ export default function NewAgentPage() {
</div>
</div>
<SkillDetailDialog
skillName={detailSkillName}
open={detailSkillName !== null}
onOpenChange={(o) => {
if (!o) setDetailSkillName(null);
}}
/>
{formError ? (
<p className="text-destructive text-sm">{formError}</p>
) : null}