Files
beaver_project/app-instance/frontend/app/(app)/skills/page.tsx
steven_li cdfc222c9f feat: 添加swarms团队编排功能并优化agent委派系统
- 引入AgentTeamOrchestrator支持多agent协同任务执行
- 增加第三方swarms库依赖并配置git协议替换以改善包管理
- 扩展DelegationManager支持团队任务调度和进度跟踪
- 实现中文bigram分词算法提升中文任务检索准确性
- 调整A2AClient和DelegationManager超时时间从30秒增至600秒
- 优化AgentRunResult状态判断逻辑增加有意义摘要检测
- 修改Dockerfile配置npm仓库镜像地址和git协议映射
- 更新CLI命令行接口支持网关端口配置传递
- 调整提供者超时配置机制增强请求稳定性
- 移除过时的support_group字段简化agent描述符结构
- 增强错误处理和进度事件报告机制改进用户体验
2026-04-14 14:34:23 +08:00

316 lines
11 KiB
TypeScript

'use client';
import React, { useEffect, useState, useRef } from 'react';
import {
Puzzle,
Upload,
Download,
Trash2,
RefreshCw,
Loader2,
AlertCircle,
X,
} from 'lucide-react';
import { listSkills, deleteSkill, uploadSkill, downloadSkill } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import type { Skill } from '@/types';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
export default function SkillsPage() {
const { locale } = useAppI18n();
const [skills, setSkills] = useState<Skill[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showUpload, setShowUpload] = useState(false);
const [deleting, setDeleting] = useState<string | null>(null);
const loadSkills = async () => {
setLoading(true);
setError(null);
try {
const data = await listSkills();
setSkills(Array.isArray(data) ? data : []);
} catch (err: any) {
setError(err.message || pickAppText(locale, '加载技能失败', 'Failed to load skills'));
} finally {
setLoading(false);
}
};
useEffect(() => {
loadSkills();
}, []);
const handleDelete = async (name: string) => {
setDeleting(name);
};
const confirmDelete = async (name: string) => {
try {
await deleteSkill(name);
setDeleting(null);
loadSkills();
} catch (err: any) {
setError(err.message || pickAppText(locale, '删除技能失败', 'Failed to delete the skill'));
setDeleting(null);
}
};
const handleUploadDone = () => {
setShowUpload(false);
loadSkills();
};
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="max-w-5xl mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold flex items-center gap-2">
<Puzzle className="w-6 h-6" />
{pickAppText(locale, '技能', 'Skills')}
</h1>
<div className="flex items-center gap-2">
<Button onClick={loadSkills} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
{pickAppText(locale, '刷新', 'Refresh')}
</Button>
<Button onClick={() => setShowUpload(true)} size="sm">
<Upload className="w-4 h-4 mr-2" />
{pickAppText(locale, '上传技能', 'Upload skill')}
</Button>
</div>
</div>
{error && (
<Card className="border-destructive">
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-destructive text-sm">
<AlertCircle className="w-4 h-4" />
{error}
</div>
</CardContent>
</Card>
)}
{/* Upload Dialog */}
{showUpload && (
<UploadSkillForm
onDone={handleUploadDone}
onCancel={() => setShowUpload(false)}
onError={(msg) => setError(msg)}
/>
)}
{/* Delete Confirmation */}
{deleting && (
<Card className="border-destructive">
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<p className="text-sm">
{pickAppText(locale, '确定删除技能', 'Delete skill')} <strong>{deleting}</strong> {pickAppText(locale, '吗?此操作不可撤销。', '? This action cannot be undone.')}
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setDeleting(null)}
>
{pickAppText(locale, '取消', 'Cancel')}
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => confirmDelete(deleting)}
>
{pickAppText(locale, '删除', 'Delete')}
</Button>
</div>
</div>
</CardContent>
</Card>
)}
{/* Skills Table */}
<Card>
<CardContent className="p-0">
{skills.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
<Puzzle className="w-10 h-10 mx-auto mb-3 opacity-30" />
<p className="font-medium">{pickAppText(locale, '暂无技能', 'No skills yet')}</p>
<p className="text-sm mt-1">{pickAppText(locale, '上传一个技能 zip 包即可开始使用。', 'Upload a skill zip package to get started.')}</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>{pickAppText(locale, '名称', 'Name')}</TableHead>
<TableHead>{pickAppText(locale, '描述', 'Description')}</TableHead>
<TableHead>{pickAppText(locale, '来源', 'Source')}</TableHead>
<TableHead>{pickAppText(locale, '状态', 'Status')}</TableHead>
<TableHead className="w-24">{pickAppText(locale, '操作', 'Actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{skills.map((skill) => (
<TableRow key={`${skill.source}:${skill.name}`}>
<TableCell className="font-medium">{skill.name}</TableCell>
<TableCell>
<span className="text-sm text-muted-foreground truncate max-w-[300px] block">
{skill.description}
</span>
</TableCell>
<TableCell>
{skill.source === 'builtin' ? (
<Badge variant="secondary" className="text-xs">
{pickAppText(locale, '内置', 'Built in')}
</Badge>
) : (
<Badge variant="default" className="text-xs">
{pickAppText(locale, '工作区', 'Workspace')}
</Badge>
)}
</TableCell>
<TableCell>
{skill.available ? (
<Badge variant="default" className="text-xs bg-green-600">
{pickAppText(locale, '可用', 'Available')}
</Badge>
) : (
<Badge variant="outline" className="text-xs text-muted-foreground">
{pickAppText(locale, '不可用', 'Unavailable')}
</Badge>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
title={pickAppText(locale, '下载', 'Download')}
onClick={() => downloadSkill(skill.name).catch((e) => setError(e.message))}
>
<Download className="w-3.5 h-3.5" />
</Button>
{skill.source === 'workspace' && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => handleDelete(skill.name)}
title={pickAppText(locale, '删除', 'Delete')}
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}
function UploadSkillForm({
onDone,
onCancel,
onError,
}: {
onDone: () => void;
onCancel: () => void;
onError: (msg: string) => void;
}) {
const { locale } = useAppI18n();
const [uploading, setUploading] = useState(false);
const fileRef = useRef<HTMLInputElement>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const file = fileRef.current?.files?.[0];
if (!file) return;
setUploading(true);
try {
await uploadSkill(file);
onDone();
} catch (err: any) {
onError(err.message || pickAppText(locale, '上传失败', 'Upload failed'));
} finally {
setUploading(false);
}
};
return (
<Card>
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-base">{pickAppText(locale, '上传技能', 'Upload skill')}</CardTitle>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={onCancel}>
<X className="w-4 h-4" />
</Button>
</div>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="skill-zip">
{pickAppText(locale, '技能压缩包', 'Skill archive')}
</label>
<input
id="skill-zip"
ref={fileRef}
type="file"
accept=".zip"
className="block w-full text-sm text-muted-foreground file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary file:text-primary-foreground hover:file:bg-primary/90 cursor-pointer"
/>
<p className="text-xs text-muted-foreground">
{pickAppText(locale, '压缩包中必须包含 `SKILL.md` 文件', 'The archive must contain a `SKILL.md` file')}
</p>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel}>
{pickAppText(locale, '取消', 'Cancel')}
</Button>
<Button type="submit" disabled={uploading}>
{uploading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{pickAppText(locale, '上传中...', 'Uploading...')}
</>
) : (
<>
<Upload className="w-4 h-4 mr-2" />
{pickAppText(locale, '上传', 'Upload')}
</>
)}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}