收口 Makelore 客户端变更
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-12 22:35:06 +08:00
parent 253bad8b40
commit f4113a872f
232 changed files with 4617 additions and 20121 deletions

View File

@@ -10,6 +10,7 @@ import {
FileCode2,
FileText,
FolderKanban,
FolderOpen,
Layers3,
Lock,
Mic,
@@ -22,11 +23,13 @@ import {
Square,
Upload,
Volume2,
Wrench,
} from 'lucide-react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { DisclosureContent } from '@/components/ui/disclosure';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select } from '@/components/ui/select';
@@ -98,6 +101,8 @@ type Skill = {
appliesTo?: string[];
statusLabel: string;
location?: string;
content?: string;
entries?: SkillStructureEntry[];
};
type OpencodeSkillInfo = {
@@ -105,8 +110,11 @@ type OpencodeSkillInfo = {
description?: string;
location: string;
content: string;
entries?: SkillStructureEntry[];
};
type SkillStructureEntry = { path: string; type: 'file' | 'directory' };
type KnowledgeFile = {
id: string;
title: string;
@@ -394,6 +402,8 @@ function mapOpencodeSkillToDisplaySkill(skill: OpencodeSkillInfo): Skill {
appliesTo: [],
statusLabel: 'opencode 已安装',
location: skill.location,
content: skill.content,
entries: skill.entries,
};
}
@@ -407,7 +417,13 @@ function mergeInstalledSkills(runtimeSkills: Skill[]): Skill[] {
const bundledSkills = courseSkills.map((skill) => {
const runtimeSkill = runtimeById.get(skill.id);
return runtimeSkill
? { ...skill, location: runtimeSkill.location, statusLabel: runtimeSkill.statusLabel }
? {
...skill,
location: runtimeSkill.location,
statusLabel: runtimeSkill.statusLabel,
content: runtimeSkill.content,
entries: runtimeSkill.entries,
}
: skill;
});
const knownIds = new Set(bundledSkills.map((skill) => skill.id));
@@ -729,7 +745,7 @@ function ResourceCard({
onClick,
}: {
id: string;
icon: string;
icon: React.ReactNode;
title: string;
subtitle: string;
locked?: boolean;
@@ -758,11 +774,15 @@ function DrawerFrame({
desc,
children,
onClose,
closeLabel = '关闭',
closeIcon = 'close',
}: {
title: string;
desc?: string;
children: React.ReactNode;
onClose: () => void;
closeLabel?: string;
closeIcon?: 'close' | 'back';
}) {
return (
<>
@@ -772,8 +792,8 @@ function DrawerFrame({
<SheetTitle className="text-foreground">{title}</SheetTitle>
{desc ? <SheetDescription className="mt-1 text-muted-foreground">{desc}</SheetDescription> : null}
</div>
<Button type="button" variant="outline" size="sm" className={whiteControlClassName} onClick={onClose}>
×
<Button type="button" variant="outline" size="sm" className={whiteControlClassName} onClick={onClose} aria-label={closeLabel}>
{closeIcon === 'back' ? <ArrowLeft className="h-4 w-4" /> : '×'}
</Button>
</div>
</SheetHeader>
@@ -784,9 +804,35 @@ 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);
@@ -856,6 +902,7 @@ export function CharacterScene() {
() => normalizeSelectedSkillIds(draftSkillIds, installedSkills),
[draftSkillIds, installedSkills],
);
const activeSkill = installedSkills.find((skill) => skill.id === activeSkillId) ?? null;
useEffect(() => {
const handleOpenProjectDialog = () => setProjectDialogOpen(true);
@@ -1184,10 +1231,13 @@ export function CharacterScene() {
/>
<ResourceCard
id="skills"
icon="⚡"
icon={<Wrench className="h-10 w-10 text-brand" />}
title="技能库"
subtitle={installedSkillsLoading ? '正在读取技能' : `已安装${installedSkills.length}个`}
onClick={() => setDrawerMode('skills')}
onClick={() => {
setActiveSkillId(null);
setDrawerMode('skills');
}}
/>
<ResourceCard
id="tools"
@@ -1261,7 +1311,12 @@ export function CharacterScene() {
</div>
<Sheet open={drawerMode !== null} onOpenChange={(open) => !open && setDrawerMode(null)}>
<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">
{drawerMode === 'models' ? (
<DrawerFrame title="LLM模型管理" desc="管理伙伴可以使用的电子心脏。" onClose={() => setDrawerMode(null)}>
@@ -1407,8 +1462,14 @@ export function CharacterScene() {
</DrawerFrame>
) : null}
{drawerMode === 'skills' ? (
<DrawerFrame title="可用技能库" desc="这里展示已安装技能;绑定请进入伙伴配置。" onClose={() => setDrawerMode(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>
@@ -1420,21 +1481,28 @@ export function CharacterScene() {
) : null}
<div className="space-y-3">
{installedSkills.map((skill) => (
<div
<button
key={skill.id}
type="button"
data-testid={`skill-library-card-${skill.id}`}
className="rounded-lg border border-foreground/15 bg-white p-3"
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="font-semibold">⚡ {skill.name}</p>
<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>
<Badge className="shrink-0 border border-foreground/15 bg-brand-soft text-foreground">
{skill.statusLabel}
</Badge>
</div>
<p className="mt-3 text-sm font-medium leading-6 text-muted-foreground">{skill.description}</p>
</div>
<p className="mt-2 text-[11px] font-medium text-brand">查看结构与 SKILL.md</p>
</button>
))}
</div>
</DrawerFrame>
@@ -1853,16 +1921,15 @@ export function ChatScene() {
<button
type="button"
onClick={() => setMemoryOpen(!memoryOpen)}
aria-expanded={memoryOpen}
className="mt-5 w-fit rounded-full border border-foreground/15 bg-surface-tertiary px-3 py-1 text-xs font-semibold shadow-soft"
>
{memoryNotice.title}
</button>
{memoryOpen ? (
<div className="mt-3 max-w-xl rounded-lg border border-foreground/15 bg-background p-4 shadow-soft">
<p className="font-semibold">本次更新</p>
<p className="mt-2 text-sm">{memoryNotice.detail}</p>
</div>
) : null}
<DisclosureContent open={memoryOpen} className="mt-3" innerClassName="max-w-xl rounded-lg border border-foreground/15 bg-background p-4 shadow-soft">
<p className="font-semibold">本次更新</p>
<p className="mt-2 text-sm">{memoryNotice.detail}</p>
</DisclosureContent>
</>
) : null}
</div>
@@ -1922,33 +1989,32 @@ export function ChatScene() {
<button
type="button"
onClick={() => setExpandedAgentDeliverables((current) => ({ ...current, [agent.id]: !expanded }))}
aria-expanded={expanded}
className="mt-3 flex w-full items-center justify-between rounded-md border border-foreground/15 bg-background px-3 py-2 text-xs font-semibold text-foreground"
>
<span>交付物({agentDeliverables.length})</span>
<span>{expanded ? '收起' : '展开'}</span>
</button>
{expanded ? (
<div className="mt-2 space-y-2">
{agentDeliverables.length > 0 ? agentDeliverables.map((deliverable) => (
<button
key={deliverable.id}
type="button"
onClick={() => navigate('/deliverables')}
className="flex w-full items-center gap-2 rounded-md border border-foreground/15 bg-white p-2 text-left text-foreground"
>
<DeliverableIcon type={deliverable.type} />
<div className="min-w-0 flex-1">
<p className="truncate text-xs font-semibold">{deliverable.title}</p>
<p className="text-[11px] text-muted-foreground">{deliverable.createdAt}</p>
</div>
</button>
)) : (
<div className="rounded-md border border-dashed border-foreground/15 bg-white p-3 text-xs font-medium text-muted-foreground">
这个Agent还没有交付物。
<DisclosureContent open={expanded} className="mt-2" innerClassName="space-y-2">
{agentDeliverables.length > 0 ? agentDeliverables.map((deliverable) => (
<button
key={deliverable.id}
type="button"
onClick={() => navigate('/deliverables')}
className="flex w-full items-center gap-2 rounded-md border border-foreground/15 bg-white p-2 text-left text-foreground"
>
<DeliverableIcon type={deliverable.type} />
<div className="min-w-0 flex-1">
<p className="truncate text-xs font-semibold">{deliverable.title}</p>
<p className="text-[11px] text-muted-foreground">{deliverable.createdAt}</p>
</div>
)}
</div>
) : null}
</button>
)) : (
<div className="rounded-md border border-dashed border-foreground/15 bg-white p-3 text-xs font-medium text-muted-foreground">
这个Agent还没有交付物。
</div>
)}
</DisclosureContent>
</div>
);
})}