feature: add generated skills to management scope

This commit is contained in:
qixinbo
2026-03-19 16:38:38 +08:00
parent 93e0bade02
commit d1c097b4d1
2 changed files with 80 additions and 19 deletions
+55 -6
View File
@@ -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
+25 -13
View File
@@ -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>