feat(plugins): merge Skill management into plugins
This commit is contained in:
1 parent
bb8a19eecf
commit
ab9f889233
49 files changed
+461
-1218
No files matched your search
+32
-194
@@ -10,7 +10,6 @@ import {
|
||||
FileCode2,
|
||||
FileText,
|
||||
FolderKanban,
|
||||
FolderOpen,
|
||||
Layers3,
|
||||
Lock,
|
||||
Mic,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
Square,
|
||||
Upload,
|
||||
Volume2,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -52,13 +50,12 @@ import {
|
||||
type CreatedPartnerSubagent,
|
||||
} from '@/stores/partner-profiles';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
import { getSkillDisplayInfo } from '@/lib/skill-display';
|
||||
|
||||
type ModelState = 'using' | 'available' | 'pending' | 'locked';
|
||||
export type DeliverableType = 'html' | 'markdown' | 'code' | 'table';
|
||||
type MessageRole = 'user' | 'assistant';
|
||||
type ChatMode = 'focus' | 'history';
|
||||
type CharacterDrawerMode = 'models' | 'knowledge' | 'skills' | 'character' | 'createPartner' | null;
|
||||
type CharacterDrawerMode = 'models' | 'knowledge' | 'character' | 'createPartner' | null;
|
||||
|
||||
const lightFieldClassName = 'border-border/70 bg-surface-input text-foreground placeholder:text-muted-foreground';
|
||||
const whiteControlClassName = 'border-border/70 bg-background text-foreground hover:bg-surface-subtle';
|
||||
@@ -94,28 +91,14 @@ type Skill = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
prompt?: string;
|
||||
inputs?: string[];
|
||||
outputs?: string[];
|
||||
guardrails?: string[];
|
||||
appliesTo?: string[];
|
||||
statusLabel: string;
|
||||
location?: string;
|
||||
content?: string;
|
||||
entries?: SkillStructureEntry[];
|
||||
};
|
||||
|
||||
type CodingSkillInfo = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
location: string;
|
||||
content: string;
|
||||
entries?: SkillStructureEntry[];
|
||||
};
|
||||
|
||||
type SkillStructureEntry = { path: string; type: 'file' | 'directory' };
|
||||
|
||||
type KnowledgeFile = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -208,53 +191,6 @@ const demoModels: DisplayModel[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const courseSkills: Skill[] = [
|
||||
{
|
||||
id: 'agent-browser',
|
||||
name: '开发浏览器',
|
||||
description: '打开、查看或调试本地及公网网页,读取 Console、Network、DOM 和样式信息。',
|
||||
prompt: '只在需要检查或调试网页时使用开发浏览器,并根据真实页面证据说明结果。',
|
||||
inputs: ['开发中的网页地址', '需要检查的页面状态'],
|
||||
outputs: ['页面检查结果', 'Console/Network/DOM 证据', '调试建议'],
|
||||
guardrails: ['只用于开发过程', '不把本地调试结果当成已发布作品', '不绕过项目浏览器工具访问页面'],
|
||||
appliesTo: ['需要网页调试的项目伙伴'],
|
||||
statusLabel: '平台内置 · 默认勾选',
|
||||
},
|
||||
{
|
||||
id: 'grilling',
|
||||
name: '方案质询',
|
||||
description: '在重要工作开始前逐项澄清目标、范围和关键取舍,确认后再执行。',
|
||||
prompt: '当任务涉及重要实现或设计决策时,先逐项确认关键选择,不要在未确认前修改项目。',
|
||||
inputs: ['用户目标', '范围与约束', '验收标准'],
|
||||
outputs: ['已确认的方案', '主要取舍', '剩余不确定性'],
|
||||
guardrails: ['每轮只问一个问题', '先查代码事实', '确认前不执行实现'],
|
||||
appliesTo: ['需要澄清复杂需求的项目伙伴'],
|
||||
statusLabel: '平台内置 · 默认勾选',
|
||||
},
|
||||
{
|
||||
id: 'planning-with-files',
|
||||
name: '项目规划',
|
||||
description: '为复杂任务维护可恢复的计划、发现和进展,直接记录在当前项目根目录。',
|
||||
prompt: '复杂任务开始前在当前项目根目录建立并持续更新 task_plan.md、findings.md 和 progress.md。',
|
||||
inputs: ['复杂任务目标', '阶段与成功标准', '代码和验证结果'],
|
||||
outputs: ['可恢复的计划', '研究发现', '进展与验证记录'],
|
||||
guardrails: ['简单任务不创建规划文件', '不记录密钥和敏感数据', '遵守项目 AGENTS.md'],
|
||||
appliesTo: ['需要持续推进复杂任务的项目伙伴'],
|
||||
statusLabel: '平台内置 · 默认勾选',
|
||||
},
|
||||
{
|
||||
id: 'frontend-slides',
|
||||
name: '项目演示',
|
||||
description: '把项目内容整理成可播放的 16:9 HTML 幻灯片,不生成 PPTX 或云端发布。',
|
||||
prompt: '当用户准备汇报、结题展示或项目介绍时,主动把真实项目内容整理成可播放的本地 HTML 幻灯片,并用浏览器检查布局和素材。',
|
||||
inputs: ['项目目标与受众', '功能截图或素材', '想讲清楚的过程与结果'],
|
||||
outputs: ['16:9 HTML 演示', '风格草图', '浏览器验收结果'],
|
||||
guardrails: ['只生成本地 HTML', '不生成 PPTX', '不部署或上传到云端', '不编造项目数据'],
|
||||
appliesTo: ['需要结题展示的项目伙伴'],
|
||||
statusLabel: '平台内置 · 可手动选择',
|
||||
},
|
||||
];
|
||||
|
||||
const knowledgeFiles: KnowledgeFile[] = [
|
||||
{
|
||||
id: 'kid-html',
|
||||
@@ -314,46 +250,18 @@ function getSubagentSkillIds(subagent: CreatedPartnerSubagent | null | undefined
|
||||
}
|
||||
|
||||
function mapCodingSkillToDisplaySkill(skill: CodingSkillInfo): Skill {
|
||||
const display = getSkillDisplayInfo(skill.name);
|
||||
return {
|
||||
id: skill.id,
|
||||
name: display.name,
|
||||
description: display.description,
|
||||
prompt: skill.content.trim().split(/\r?\n/).find((line) => line.trim() && !line.startsWith('---'))?.trim() || '从本地技能目录加载。',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
guardrails: [],
|
||||
appliesTo: [],
|
||||
statusLabel: '本地已安装',
|
||||
location: skill.location,
|
||||
content: skill.content,
|
||||
entries: skill.entries,
|
||||
name: skill.name,
|
||||
description: skill.description ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSelectedSkillIds(skillIds: string[], availableSkills: Skill[] = courseSkills): string[] {
|
||||
function normalizeSelectedSkillIds(skillIds: string[], availableSkills: Skill[] = []): string[] {
|
||||
const selected = new Set(skillIds);
|
||||
return availableSkills.map((skill) => skill.id).filter((skillId) => selected.has(skillId));
|
||||
}
|
||||
|
||||
function mergeInstalledSkills(runtimeSkills: Skill[]): Skill[] {
|
||||
const runtimeById = new Map(runtimeSkills.map((skill) => [skill.id, skill]));
|
||||
const bundledSkills = courseSkills.map((skill) => {
|
||||
const runtimeSkill = runtimeById.get(skill.id);
|
||||
return runtimeSkill
|
||||
? {
|
||||
...skill,
|
||||
location: runtimeSkill.location,
|
||||
statusLabel: runtimeSkill.statusLabel,
|
||||
content: runtimeSkill.content,
|
||||
entries: runtimeSkill.entries,
|
||||
}
|
||||
: skill;
|
||||
});
|
||||
const knownIds = new Set(bundledSkills.map((skill) => skill.id));
|
||||
return [...bundledSkills, ...runtimeSkills.filter((skill) => !knownIds.has(skill.id))];
|
||||
}
|
||||
|
||||
const fallbackCharacter: Character = {
|
||||
id: 'custom',
|
||||
name: '自定义伙伴',
|
||||
@@ -728,35 +636,9 @@ function DrawerFrame({
|
||||
);
|
||||
}
|
||||
|
||||
function SkillDetail({ skill }: { skill: Skill }) {
|
||||
const entries = skill.entries ?? [{ path: 'SKILL.md', type: 'file' as const }];
|
||||
return (
|
||||
<div data-testid="skill-detail-view" className="space-y-4">
|
||||
<section data-testid="skill-structure" className="rounded-xl border border-foreground/15 bg-surface-tertiary p-4">
|
||||
<div className="flex items-center gap-2"><FolderOpen className="h-4 w-4 text-brand" /><h3 className="font-semibold">Skill 内结构</h3></div>
|
||||
<p className="mt-1 truncate text-[11px] font-medium text-muted-foreground" title={skill.location}>{skill.location ?? '运行时 Skill 目录'}</p>
|
||||
<div className="mt-3 space-y-1 rounded-lg border border-foreground/10 bg-white p-2">
|
||||
{entries.map((entry) => {
|
||||
const depth = entry.path.split('/').length - 1;
|
||||
return <div key={`${entry.type}:${entry.path}`} className="flex items-center gap-2 rounded-md py-1 text-xs font-medium text-foreground" style={{ paddingLeft: `${8 + depth * 14}px` }}>
|
||||
{entry.type === 'directory' ? <FolderOpen className="h-3.5 w-3.5 shrink-0 text-brand" /> : <FileText className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />}
|
||||
<code className="truncate">{entry.type === 'directory' ? `${entry.path}/` : entry.path}</code>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
<section data-testid="skill-main-file" className="rounded-xl border border-foreground/15 bg-white p-4">
|
||||
<div className="flex items-center gap-2"><FileText className="h-4 w-4 text-brand" /><h3 className="font-semibold">SKILL.md</h3></div>
|
||||
<pre className="mt-3 max-h-[min(58vh,680px)] overflow-auto whitespace-pre-wrap break-words rounded-lg border border-foreground/10 bg-surface-tertiary p-3 font-mono text-[11px] leading-5 text-foreground">{skill.content || 'SKILL.md 暂无内容。'}</pre>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CharacterScene() {
|
||||
const navigate = useNavigate();
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [activeSkillId, setActiveSkillId] = useState<string | null>(null);
|
||||
const [drawerMode, setDrawerMode] = useState<CharacterDrawerMode>(null);
|
||||
const [projectDialogOpen, setProjectDialogOpen] = useState(false);
|
||||
const partnerSubagents = usePartnerProfilesStore((state) => state.subagents);
|
||||
@@ -817,7 +699,7 @@ export function CharacterScene() {
|
||||
const [knowledgeItems, setKnowledgeItems] = useState<KnowledgeFile[]>(knowledgeFiles);
|
||||
const [knowledgeNotice, setKnowledgeNotice] = useState<string | null>(null);
|
||||
const [knowledgeDragActive, setKnowledgeDragActive] = useState(false);
|
||||
const [installedSkills, setInstalledSkills] = useState<Skill[]>(courseSkills);
|
||||
const [installedSkills, setInstalledSkills] = useState<Skill[]>([]);
|
||||
const [installedSkillsLoading, setInstalledSkillsLoading] = useState(false);
|
||||
const [installedSkillsError, setInstalledSkillsError] = useState<string | null>(null);
|
||||
const knowledgeFileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -827,7 +709,6 @@ export function CharacterScene() {
|
||||
() => normalizeSelectedSkillIds(draftSkillIds, installedSkills),
|
||||
[draftSkillIds, installedSkills],
|
||||
);
|
||||
const activeSkill = installedSkills.find((skill) => skill.id === activeSkillId) ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
const handleOpenProjectDialog = () => setProjectDialogOpen(true);
|
||||
@@ -837,12 +718,12 @@ export function CharacterScene() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeProject) {
|
||||
setInstalledSkills(courseSkills);
|
||||
setInstalledSkills([]);
|
||||
setInstalledSkillsLoading(false);
|
||||
setInstalledSkillsError(null);
|
||||
return;
|
||||
}
|
||||
if (drawerMode !== 'skills' && drawerMode !== 'character') {
|
||||
if (drawerMode !== 'character') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -855,17 +736,17 @@ export function CharacterScene() {
|
||||
if (cancelled) return;
|
||||
const runtimeSkills = Array.isArray(payload?.skills) ? payload.skills : [];
|
||||
if (runtimeSkills.length === 0) {
|
||||
setInstalledSkills(courseSkills);
|
||||
setInstalledSkillsError('当前没有返回已安装技能,正在显示课程内置技能。');
|
||||
setInstalledSkills([]);
|
||||
setInstalledSkillsError(null);
|
||||
return;
|
||||
}
|
||||
setInstalledSkills(mergeInstalledSkills(runtimeSkills.map(mapCodingSkillToDisplaySkill)));
|
||||
setInstalledSkills(runtimeSkills.map(mapCodingSkillToDisplaySkill));
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
console.warn('[makelore] Failed to load coding skills', error);
|
||||
setInstalledSkills(courseSkills);
|
||||
setInstalledSkillsError('暂时无法读取已安装技能,正在显示课程内置技能。');
|
||||
setInstalledSkills([]);
|
||||
setInstalledSkillsError('暂时无法读取已安装的插件能力。');
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setInstalledSkillsLoading(false);
|
||||
@@ -1159,14 +1040,11 @@ export function CharacterScene() {
|
||||
}}
|
||||
/>
|
||||
<ResourceCard
|
||||
id="skills"
|
||||
icon={<Wrench className="h-10 w-10 text-brand" />}
|
||||
title="技能库"
|
||||
subtitle={installedSkillsLoading ? '正在读取技能' : `已安装${installedSkills.length}个`}
|
||||
onClick={() => {
|
||||
setActiveSkillId(null);
|
||||
setDrawerMode('skills');
|
||||
}}
|
||||
id="plugins"
|
||||
icon="🧩"
|
||||
title="插件"
|
||||
subtitle="可选与已安装"
|
||||
onClick={() => navigate('/project-config/plugins?scope=all&source=all&state=available')}
|
||||
/>
|
||||
<ResourceCard
|
||||
id="tools"
|
||||
@@ -1178,7 +1056,7 @@ export function CharacterScene() {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Panel title="我的伙伴" desc="从这里创建和保存手动配置的伙伴;项目使用时会读取你保存的提示词和技能。">
|
||||
<Panel title="我的伙伴" desc="从这里创建和保存手动配置的伙伴;项目使用时会读取你保存的提示词和插件能力。">
|
||||
<div className="mb-3 flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -1228,9 +1106,7 @@ export function CharacterScene() {
|
||||
<p className="mt-1 text-xs font-medium text-muted-foreground">{baseCharacter.name} · {character.role}</p>
|
||||
<p className="mt-1 truncate font-mono text-[11px] font-medium text-muted-foreground">{character.id}</p>
|
||||
<p className="mt-2 line-clamp-2 min-h-8 text-xs text-muted-foreground">{character.customInfo}</p>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">
|
||||
{getSubagentSkillIds(subagent).length} 个技能
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">插件能力按项目配置</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -1243,7 +1119,6 @@ export function CharacterScene() {
|
||||
<Sheet open={drawerMode !== null} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDrawerMode(null);
|
||||
setActiveSkillId(null);
|
||||
}
|
||||
}}>
|
||||
<SheetContent className="flex w-[min(430px,94vw)] flex-col overflow-hidden border-l border-foreground/15 bg-background p-0 text-foreground sm:max-w-none">
|
||||
@@ -1399,52 +1274,6 @@ export function CharacterScene() {
|
||||
</DrawerFrame>
|
||||
) : null}
|
||||
|
||||
{drawerMode === 'skills' && activeSkill ? (
|
||||
<DrawerFrame title={`${activeSkill.name} · 技能详情`} desc="先查看 Skill 内结构,再阅读主文件 SKILL.md。" onClose={() => setActiveSkillId(null)} closeLabel="返回技能列表" closeIcon="back">
|
||||
<SkillDetail skill={activeSkill} />
|
||||
</DrawerFrame>
|
||||
) : null}
|
||||
|
||||
{drawerMode === 'skills' && !activeSkill ? (
|
||||
<DrawerFrame title="可用技能库" desc="点击技能查看文件结构和主文件内容;绑定请进入伙伴配置。" onClose={() => setDrawerMode(null)}>
|
||||
<div className="mb-4 flex items-center justify-between rounded-lg border border-foreground/15 bg-surface-tertiary px-3 py-2">
|
||||
<span className="text-sm font-semibold text-foreground">技能列表</span>
|
||||
<Badge className="border border-foreground/15 bg-white text-foreground">{installedSkills.length} 个技能</Badge>
|
||||
</div>
|
||||
{installedSkillsError ? (
|
||||
<div className="mb-3 rounded-md border border-foreground/15 bg-surface-tertiary px-3 py-2 text-sm font-medium text-foreground">
|
||||
{installedSkillsError}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="space-y-3">
|
||||
{installedSkills.map((skill) => (
|
||||
<button
|
||||
key={skill.id}
|
||||
type="button"
|
||||
data-testid={`skill-library-card-${skill.id}`}
|
||||
aria-label={`查看技能详情:${skill.name}`}
|
||||
onClick={() => setActiveSkillId(skill.id)}
|
||||
className="w-full rounded-lg border border-foreground/15 bg-white p-3 text-left transition hover:border-brand/40 hover:bg-brand-soft/30"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="flex items-center gap-2 font-semibold"><Wrench className="h-4 w-4 shrink-0 text-brand" />{skill.name}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="shrink-0 border border-foreground/15 bg-brand-soft text-foreground">
|
||||
{skill.statusLabel}
|
||||
</Badge>
|
||||
<ArrowLeft className="h-4 w-4 shrink-0 rotate-180 text-muted-foreground" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-sm font-medium leading-6 text-muted-foreground">{skill.description}</p>
|
||||
<p className="mt-2 text-[11px] font-medium text-brand">查看结构与 SKILL.md</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</DrawerFrame>
|
||||
) : null}
|
||||
|
||||
{drawerMode === 'createPartner' ? (
|
||||
<DrawerFrame title="新增伙伴" desc="所有伙伴都由你手动配置;不会自动套用任何角色预设。" onClose={() => setDrawerMode(null)}>
|
||||
<form className="space-y-4" onSubmit={handleCreatePartner}>
|
||||
@@ -1484,7 +1313,7 @@ export function CharacterScene() {
|
||||
) : null}
|
||||
|
||||
{drawerMode === 'character' ? (
|
||||
<DrawerFrame title={active.name} desc="编辑展示资料和运行时技能绑定。保存后会写入当前项目的伙伴配置。" onClose={() => setDrawerMode(null)}>
|
||||
<DrawerFrame title={active.name} desc="编辑展示资料和插件能力绑定。保存后会写入当前项目的伙伴配置。" onClose={() => setDrawerMode(null)}>
|
||||
<div data-testid={`partner-profile-editor-${activeRuntimeId}`}>
|
||||
<div className="flex flex-col items-center rounded-lg border border-foreground/15 bg-surface-tertiary p-4">
|
||||
<PixelAvatar
|
||||
@@ -1580,9 +1409,9 @@ export function CharacterScene() {
|
||||
<div className="mt-4 rounded-lg border border-foreground/15 bg-white p-3 text-foreground">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="font-semibold">绑定技能</p>
|
||||
<p className="font-semibold">绑定插件能力</p>
|
||||
<p className="mt-1 text-xs font-medium text-muted-foreground">
|
||||
勾选后会写入伙伴的技能权限;不勾选任何技能时会禁止加载技能。
|
||||
这里只显示插件提供、且支持按伙伴分配的能力;安装与启停请前往插件模块。
|
||||
</p>
|
||||
</div>
|
||||
<Badge className="shrink-0 border border-foreground/15 bg-accent-soft text-foreground">
|
||||
@@ -1590,6 +1419,15 @@ export function CharacterScene() {
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2" data-testid={`partner-skill-bindings-${activeRuntimeId}`}>
|
||||
{installedSkillsLoading ? (
|
||||
<p className="text-xs text-muted-foreground">正在读取插件能力…</p>
|
||||
) : null}
|
||||
{installedSkillsError ? (
|
||||
<p className="text-xs text-muted-foreground">{installedSkillsError}</p>
|
||||
) : null}
|
||||
{!installedSkillsLoading && !installedSkillsError && installedSkills.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">当前没有可分配的插件能力。</p>
|
||||
) : null}
|
||||
{installedSkills.map((skill) => {
|
||||
const checked = effectiveDraftSkillIds.includes(skill.id);
|
||||
return (
|
||||
@@ -1604,7 +1442,7 @@ export function CharacterScene() {
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1 h-4 w-4 accent-brand"
|
||||
aria-label={`绑定技能:${skill.name}`}
|
||||
aria-label={`绑定插件能力:${skill.name}`}
|
||||
checked={checked}
|
||||
onChange={() => handleToggleDraftSkill(skill.id)}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user