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")
|
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., 安全, 低风险)")
|
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")
|
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):
|
class SkillCreate(BaseModel):
|
||||||
id: str
|
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]]:
|
def load_skills(project_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||||
data = _load_data()
|
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:
|
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
|
return data
|
||||||
|
|
||||||
@router.get("/skills", response_model=List[Skill])
|
@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)
|
@router.get("/skills/{skill_id}", response_model=Skill)
|
||||||
def get_skill(skill_id: str, project_id: Optional[int] = None):
|
def get_skill(skill_id: str, project_id: Optional[int] = None):
|
||||||
data = _load_data()
|
data = load_skills()
|
||||||
for item in data:
|
for item in data:
|
||||||
if item["id"] == skill_id:
|
if item["id"] == skill_id:
|
||||||
if project_id is not None and item.get("project_id") != project_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)
|
shutil.copy2(s, d)
|
||||||
|
|
||||||
# Register in skills.json
|
# Register in skills.json
|
||||||
data = _load_data()
|
data = load_skills()
|
||||||
new_skill = {
|
new_skill = {
|
||||||
"id": final_skill_id,
|
"id": final_skill_id,
|
||||||
"name": skill_name,
|
"name": skill_name,
|
||||||
@@ -276,7 +323,7 @@ async def upload_skill(
|
|||||||
|
|
||||||
@router.post("/skills", response_model=Skill)
|
@router.post("/skills", response_model=Skill)
|
||||||
def create_skill(skill: SkillCreate):
|
def create_skill(skill: SkillCreate):
|
||||||
data = _load_data()
|
data = load_skills()
|
||||||
if any(item["id"] == skill.id for item in data):
|
if any(item["id"] == skill.id for item in data):
|
||||||
raise HTTPException(status_code=400, detail="Skill with this ID already exists")
|
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)
|
@router.put("/skills/{skill_id}", response_model=Skill)
|
||||||
def update_skill(skill_id: str, skill: SkillUpdate, project_id: Optional[int] = None):
|
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):
|
for i, item in enumerate(data):
|
||||||
if item["id"] == skill_id:
|
if item["id"] == skill_id:
|
||||||
if project_id is not None and item.get("project_id") != project_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}")
|
@router.delete("/skills/{skill_id}")
|
||||||
def delete_skill(skill_id: str, project_id: Optional[int] = None):
|
def delete_skill(skill_id: str, project_id: Optional[int] = None):
|
||||||
data = _load_data()
|
data = load_skills()
|
||||||
initial_len = len(data)
|
initial_len = len(data)
|
||||||
|
|
||||||
# If project_id is provided, we only delete if it matches
|
# 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:
|
for item in data:
|
||||||
if item["id"] == skill_id:
|
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:
|
if project_id is not None and item.get("project_id") != project_id:
|
||||||
new_data.append(item)
|
new_data.append(item)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface Skill {
|
|||||||
installation_time: string;
|
installation_time: string;
|
||||||
status: string;
|
status: string;
|
||||||
file_path?: string;
|
file_path?: string;
|
||||||
|
is_builtin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Skills() {
|
export function Skills() {
|
||||||
@@ -230,19 +231,23 @@ export function Skills() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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)}
|
onClick={() => handleEditSkill(skill)}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{!skill.is_builtin ? (
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="h-8 w-8 text-zinc-400 hover:text-rose-600 hover:bg-rose-50 rounded-md transition-all"
|
size="icon"
|
||||||
onClick={() => handleDeleteSkill(skill.id)}
|
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>
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="h-8 w-8 shrink-0" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</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">
|
<DialogContent className="sm:max-w-[600px] max-h-[90vh] flex flex-col rounded-2xl p-0 overflow-hidden">
|
||||||
<DialogHeader className="p-6 pb-2">
|
<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>
|
</DialogHeader>
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-2">
|
<div className="flex-1 overflow-y-auto px-6 py-2">
|
||||||
<div className="grid gap-5">
|
<div className="grid gap-5">
|
||||||
@@ -287,6 +292,7 @@ export function Skills() {
|
|||||||
value={newSkill.name || ''}
|
value={newSkill.name || ''}
|
||||||
onChange={(e) => setNewSkill({...newSkill, name: e.target.value})}
|
onChange={(e) => setNewSkill({...newSkill, name: e.target.value})}
|
||||||
className="rounded-lg border-zinc-200 h-10"
|
className="rounded-lg border-zinc-200 h-10"
|
||||||
|
disabled={editingSkill?.is_builtin}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@@ -295,6 +301,7 @@ export function Skills() {
|
|||||||
<Select
|
<Select
|
||||||
value={newSkill.type}
|
value={newSkill.type}
|
||||||
onValueChange={(val: any) => setNewSkill({...newSkill, type: val})}
|
onValueChange={(val: any) => setNewSkill({...newSkill, type: val})}
|
||||||
|
disabled={editingSkill?.is_builtin}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="rounded-lg border-zinc-200 h-10">
|
<SelectTrigger className="rounded-lg border-zinc-200 h-10">
|
||||||
<SelectValue placeholder="选择类型" />
|
<SelectValue placeholder="选择类型" />
|
||||||
@@ -311,6 +318,7 @@ export function Skills() {
|
|||||||
<Select
|
<Select
|
||||||
value={newSkill.status}
|
value={newSkill.status}
|
||||||
onValueChange={(val: any) => setNewSkill({...newSkill, status: val})}
|
onValueChange={(val: any) => setNewSkill({...newSkill, status: val})}
|
||||||
|
disabled={editingSkill?.is_builtin}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="rounded-lg border-zinc-200 h-10">
|
<SelectTrigger className="rounded-lg border-zinc-200 h-10">
|
||||||
<SelectValue placeholder="选择状态" />
|
<SelectValue placeholder="选择状态" />
|
||||||
@@ -330,6 +338,7 @@ export function Skills() {
|
|||||||
value={newSkill.description || ''}
|
value={newSkill.description || ''}
|
||||||
onChange={(e) => setNewSkill({...newSkill, description: e.target.value})}
|
onChange={(e) => setNewSkill({...newSkill, description: e.target.value})}
|
||||||
className="rounded-lg border-zinc-200 min-h-[80px] py-2 text-sm"
|
className="rounded-lg border-zinc-200 min-h-[80px] py-2 text-sm"
|
||||||
|
disabled={editingSkill?.is_builtin}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
@@ -340,14 +349,17 @@ export function Skills() {
|
|||||||
onChange={(e) => setNewSkill({...newSkill, content: e.target.value})}
|
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"
|
className="rounded-lg border-zinc-200 font-mono text-xs min-h-[160px] py-3 bg-zinc-50"
|
||||||
placeholder="Python 代码、SQL 查询模板或 API 规范..."
|
placeholder="Python 代码、SQL 查询模板或 API 规范..."
|
||||||
|
disabled={editingSkill?.is_builtin}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="p-6 pt-2">
|
<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">
|
{!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>
|
保存技能
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user