feature: add generated skills to management scope
This commit is contained in:
@@ -30,6 +30,7 @@ class Skill(BaseModel):
|
||||
installation_time: str = Field(default_factory=lambda: datetime.now().strftime("%Y年%m月%d日"), description="Time when the skill was installed")
|
||||
status: str = Field("安全", description="Security status of the skill (e.g., 安全, 低风险)")
|
||||
file_path: Optional[str] = Field(None, description="Path to the skill folder in skill-hub")
|
||||
is_builtin: bool = Field(False, description="Whether this is a system builtin skill")
|
||||
|
||||
class SkillCreate(BaseModel):
|
||||
id: str
|
||||
@@ -134,8 +135,54 @@ def _write_skill_markdown(skill_dir: str, skill_name: str, description: Optional
|
||||
|
||||
def load_skills(project_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
data = _load_data()
|
||||
|
||||
registered_paths = set()
|
||||
|
||||
# Sync registered skills with their SKILL.md if available
|
||||
for item in data:
|
||||
item.setdefault("is_builtin", False)
|
||||
if item.get("file_path"):
|
||||
abs_path = os.path.abspath(item["file_path"])
|
||||
registered_paths.add(abs_path)
|
||||
skill_md_path = os.path.join(abs_path, "SKILL.md")
|
||||
if os.path.exists(skill_md_path):
|
||||
metadata_res = _parse_skill_md(skill_md_path)
|
||||
if metadata_res.get("name"):
|
||||
item["name"] = metadata_res["name"]
|
||||
if metadata_res.get("description"):
|
||||
item["description"] = metadata_res["description"]
|
||||
if metadata_res.get("content"):
|
||||
item["content"] = metadata_res["content"]
|
||||
|
||||
# Scan for unregistered skills in SKILL_HUB_DIR
|
||||
if os.path.exists(SKILL_HUB_DIR):
|
||||
for item in os.listdir(SKILL_HUB_DIR):
|
||||
skill_dir = os.path.abspath(os.path.join(SKILL_HUB_DIR, item))
|
||||
if os.path.isdir(skill_dir):
|
||||
skill_md_path = os.path.join(skill_dir, "SKILL.md")
|
||||
if os.path.exists(skill_md_path) and skill_dir not in registered_paths:
|
||||
metadata_res = _parse_skill_md(skill_md_path)
|
||||
skill_name = metadata_res.get("name") or item
|
||||
|
||||
# Create a new entry for this discovered skill
|
||||
new_skill = {
|
||||
"id": item,
|
||||
"name": skill_name,
|
||||
"description": metadata_res.get("description") or "No description provided",
|
||||
"content": metadata_res.get("content") or "",
|
||||
"type": "agentskill",
|
||||
"project_id": None,
|
||||
"source": "后台生成",
|
||||
"installation_time": datetime.now().strftime("%Y年%m月%d日"),
|
||||
"status": "安全",
|
||||
"file_path": skill_dir,
|
||||
"is_builtin": item in ("nl2sql", "visualization")
|
||||
}
|
||||
data.append(new_skill)
|
||||
registered_paths.add(skill_dir)
|
||||
|
||||
if project_id is not None:
|
||||
return [item for item in data if item.get("project_id") == project_id]
|
||||
return [item for item in data if item.get("project_id") == project_id or item.get("project_id") is None]
|
||||
return data
|
||||
|
||||
@router.get("/skills", response_model=List[Skill])
|
||||
@@ -145,7 +192,7 @@ def list_skills(project_id: Optional[int] = None):
|
||||
|
||||
@router.get("/skills/{skill_id}", response_model=Skill)
|
||||
def get_skill(skill_id: str, project_id: Optional[int] = None):
|
||||
data = _load_data()
|
||||
data = load_skills()
|
||||
for item in data:
|
||||
if item["id"] == skill_id:
|
||||
if project_id is not None and item.get("project_id") != project_id:
|
||||
@@ -243,7 +290,7 @@ async def upload_skill(
|
||||
shutil.copy2(s, d)
|
||||
|
||||
# Register in skills.json
|
||||
data = _load_data()
|
||||
data = load_skills()
|
||||
new_skill = {
|
||||
"id": final_skill_id,
|
||||
"name": skill_name,
|
||||
@@ -276,7 +323,7 @@ async def upload_skill(
|
||||
|
||||
@router.post("/skills", response_model=Skill)
|
||||
def create_skill(skill: SkillCreate):
|
||||
data = _load_data()
|
||||
data = load_skills()
|
||||
if any(item["id"] == skill.id for item in data):
|
||||
raise HTTPException(status_code=400, detail="Skill with this ID already exists")
|
||||
|
||||
@@ -299,7 +346,7 @@ def create_skill(skill: SkillCreate):
|
||||
|
||||
@router.put("/skills/{skill_id}", response_model=Skill)
|
||||
def update_skill(skill_id: str, skill: SkillUpdate, project_id: Optional[int] = None):
|
||||
data = _load_data()
|
||||
data = load_skills()
|
||||
for i, item in enumerate(data):
|
||||
if item["id"] == skill_id:
|
||||
if project_id is not None and item.get("project_id") != project_id:
|
||||
@@ -321,7 +368,7 @@ def update_skill(skill_id: str, skill: SkillUpdate, project_id: Optional[int] =
|
||||
|
||||
@router.delete("/skills/{skill_id}")
|
||||
def delete_skill(skill_id: str, project_id: Optional[int] = None):
|
||||
data = _load_data()
|
||||
data = load_skills()
|
||||
initial_len = len(data)
|
||||
|
||||
# If project_id is provided, we only delete if it matches
|
||||
@@ -331,6 +378,8 @@ def delete_skill(skill_id: str, project_id: Optional[int] = None):
|
||||
|
||||
for item in data:
|
||||
if item["id"] == skill_id:
|
||||
if item.get("is_builtin"):
|
||||
raise HTTPException(status_code=400, detail="Builtin skills cannot be deleted")
|
||||
if project_id is not None and item.get("project_id") != project_id:
|
||||
new_data.append(item)
|
||||
continue
|
||||
|
||||
@@ -24,6 +24,7 @@ interface Skill {
|
||||
installation_time: string;
|
||||
status: string;
|
||||
file_path?: string;
|
||||
is_builtin?: boolean;
|
||||
}
|
||||
|
||||
export function Skills() {
|
||||
@@ -230,19 +231,23 @@ export function Skills() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-zinc-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-md transition-all"
|
||||
className="h-8 w-8 text-zinc-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-md transition-all shrink-0"
|
||||
onClick={() => handleEditSkill(skill)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-zinc-400 hover:text-rose-600 hover:bg-rose-50 rounded-md transition-all"
|
||||
onClick={() => handleDeleteSkill(skill.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
{!skill.is_builtin ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-zinc-400 hover:text-rose-600 hover:bg-rose-50 rounded-md transition-all shrink-0"
|
||||
onClick={() => handleDeleteSkill(skill.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<div className="h-8 w-8 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -275,7 +280,7 @@ export function Skills() {
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] flex flex-col rounded-2xl p-0 overflow-hidden">
|
||||
<DialogHeader className="p-6 pb-2">
|
||||
<DialogTitle className="text-xl font-bold text-zinc-900">{editingSkill ? '编辑技能' : '添加新技能'}</DialogTitle>
|
||||
<DialogTitle className="text-xl font-bold text-zinc-900">{editingSkill ? '查看/编辑技能' : '添加新技能'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-2">
|
||||
<div className="grid gap-5">
|
||||
@@ -287,6 +292,7 @@ export function Skills() {
|
||||
value={newSkill.name || ''}
|
||||
onChange={(e) => setNewSkill({...newSkill, name: e.target.value})}
|
||||
className="rounded-lg border-zinc-200 h-10"
|
||||
disabled={editingSkill?.is_builtin}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -295,6 +301,7 @@ export function Skills() {
|
||||
<Select
|
||||
value={newSkill.type}
|
||||
onValueChange={(val: any) => setNewSkill({...newSkill, type: val})}
|
||||
disabled={editingSkill?.is_builtin}
|
||||
>
|
||||
<SelectTrigger className="rounded-lg border-zinc-200 h-10">
|
||||
<SelectValue placeholder="选择类型" />
|
||||
@@ -311,6 +318,7 @@ export function Skills() {
|
||||
<Select
|
||||
value={newSkill.status}
|
||||
onValueChange={(val: any) => setNewSkill({...newSkill, status: val})}
|
||||
disabled={editingSkill?.is_builtin}
|
||||
>
|
||||
<SelectTrigger className="rounded-lg border-zinc-200 h-10">
|
||||
<SelectValue placeholder="选择状态" />
|
||||
@@ -330,6 +338,7 @@ export function Skills() {
|
||||
value={newSkill.description || ''}
|
||||
onChange={(e) => setNewSkill({...newSkill, description: e.target.value})}
|
||||
className="rounded-lg border-zinc-200 min-h-[80px] py-2 text-sm"
|
||||
disabled={editingSkill?.is_builtin}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
@@ -340,14 +349,17 @@ export function Skills() {
|
||||
onChange={(e) => setNewSkill({...newSkill, content: e.target.value})}
|
||||
className="rounded-lg border-zinc-200 font-mono text-xs min-h-[160px] py-3 bg-zinc-50"
|
||||
placeholder="Python 代码、SQL 查询模板或 API 规范..."
|
||||
disabled={editingSkill?.is_builtin}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="p-6 pt-2">
|
||||
<Button onClick={handleAddSkill} className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg px-6 h-10 w-full">
|
||||
保存技能
|
||||
</Button>
|
||||
{!editingSkill?.is_builtin && (
|
||||
<Button onClick={handleAddSkill} className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg px-6 h-10 w-full">
|
||||
保存技能
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user