1118 lines
28 KiB
Markdown
1118 lines
28 KiB
Markdown
# Skill System Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Build a Skill system where Agent can autonomously call Skills based on LLM judgment.
|
||
|
||
**Architecture:** Skill is an "ability plugin" for Agents - a combination of instructions + tools. LLM decides when to use which Skill. Skills are stored in DB, loaded at runtime, and injected into Agent context.
|
||
|
||
**Tech Stack:** FastAPI (backend), Vue 3 + Pinia (frontend), SQLAlchemy (ORM), existing LangGraph agent system
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
### Backend (New Files)
|
||
- `backend/app/models/skill.py` - Skill ORM model
|
||
- `backend/app/schemas/skill.py` - Pydantic schemas for API
|
||
- `backend/app/services/skill_service.py` - Business logic
|
||
- `backend/app/routers/skill.py` - API endpoints
|
||
- `backend/app/agents/skill_registry.py` - Loads skills for agents at runtime
|
||
- `backend/app/agents/skill_executor.py` - Executes skill instructions with tools
|
||
|
||
### Backend (Modified Files)
|
||
- `backend/app/main.py` - Register skill_router
|
||
- `backend/app/routers/__init__.py` - Export skill_router
|
||
|
||
### Frontend (New Files)
|
||
- `frontend/src/api/skill.ts` - Skill API client
|
||
- `frontend/src/views/SkillView.vue` - Skill management UI
|
||
- `frontend/src/stores/skill.ts` - Skill state (optional, if needed)
|
||
|
||
### Frontend (Modified Files)
|
||
- `frontend/src/router/index.ts` - Add /skills route
|
||
- `frontend/src/components/SidebarNav.vue` - Add "Skill 市场" nav item
|
||
|
||
---
|
||
|
||
## Task 1: Skill Model & Database
|
||
|
||
**Files:**
|
||
- Create: `backend/app/models/skill.py`
|
||
- Modify: `backend/app/database.py` (auto-import)
|
||
|
||
- [ ] **Step 1: Create Skill model**
|
||
|
||
```python
|
||
# backend/app/models/skill.py
|
||
from sqlalchemy import Column, String, Text, Boolean, JSON, ForeignKey
|
||
from sqlalchemy.orm import relationship
|
||
from app.models.base import BaseModel
|
||
|
||
|
||
class Skill(BaseModel):
|
||
__tablename__ = "skills"
|
||
|
||
name = Column(String(100), nullable=False, unique=True, index=True)
|
||
description = Column(Text, nullable=True) # 供 LLM 理解用途
|
||
instructions = Column(Text, nullable=False) # Agent 执行时的指令模板
|
||
agent_type = Column(String(50), nullable=False, index=True) # master/planner/executor/librarian/analyst
|
||
tools = Column(JSON, default=list) # 引用的工具名称列表
|
||
required_context = Column(JSON, default=list) # 需要的前置数据
|
||
output_format = Column(Text, nullable=True) # 输出格式要求
|
||
visibility = Column(String(20), default="private") # private/team/market
|
||
team_id = Column(String(36), ForeignKey("users.id"), nullable=True)
|
||
is_active = Column(Boolean, default=True)
|
||
owner_id = Column(String(36), ForeignKey("users.id"), nullable=False)
|
||
|
||
owner = relationship("User", foreign_keys=[owner_id])
|
||
team = relationship("User", foreign_keys=[team_id])
|
||
```
|
||
|
||
- [ ] **Step 2: Run database migration (auto-create table)**
|
||
|
||
Run: `cd backend && python -c "from app.database import engine, Base; import app.models.skill; Base.metadata.create_all(engine); print('Table created')"`
|
||
|
||
Expected: "Table created"
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add backend/app/models/skill.py
|
||
git commit -m "feat: add Skill model"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Skill Schema (Pydantic)
|
||
|
||
**Files:**
|
||
- Create: `backend/app/schemas/skill.py`
|
||
|
||
- [ ] **Step 1: Create Skill schemas**
|
||
|
||
```python
|
||
# backend/app/schemas/skill.py
|
||
from pydantic import BaseModel
|
||
from typing import Optional
|
||
|
||
|
||
class SkillCreate(BaseModel):
|
||
name: str
|
||
description: Optional[str] = None
|
||
instructions: str
|
||
agent_type: str # master/planner/executor/librarian/analyst
|
||
tools: list[str] = []
|
||
required_context: list[str] = []
|
||
output_format: Optional[str] = None
|
||
visibility: str = "private"
|
||
team_id: Optional[str] = None
|
||
is_active: bool = True
|
||
|
||
|
||
class SkillUpdate(BaseModel):
|
||
name: Optional[str] = None
|
||
description: Optional[str] = None
|
||
instructions: Optional[str] = None
|
||
agent_type: Optional[str] = None
|
||
tools: Optional[list[str]] = None
|
||
required_context: Optional[list[str]] = None
|
||
output_format: Optional[str] = None
|
||
visibility: Optional[str] = None
|
||
team_id: Optional[str] = None
|
||
is_active: Optional[bool] = None
|
||
|
||
|
||
class SkillOut(BaseModel):
|
||
id: str
|
||
name: str
|
||
description: Optional[str]
|
||
instructions: str
|
||
agent_type: str
|
||
tools: list[str]
|
||
required_context: list[str]
|
||
output_format: Optional[str]
|
||
visibility: str
|
||
team_id: Optional[str]
|
||
is_active: bool
|
||
owner_id: str
|
||
created_at: str
|
||
updated_at: str
|
||
|
||
model_config = {"from_attributes": True}
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add backend/app/schemas/skill.py
|
||
git commit -m "feat: add Skill Pydantic schemas"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: Skill Service
|
||
|
||
**Files:**
|
||
- Create: `backend/app/services/skill_service.py`
|
||
|
||
- [ ] **Step 1: Create SkillService class**
|
||
|
||
```python
|
||
# backend/app/services/skill_service.py
|
||
from typing import Optional
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, and_, or_
|
||
from app.models.skill import Skill
|
||
from app.models.user import User
|
||
|
||
|
||
class SkillService:
|
||
def __init__(self, db: AsyncSession):
|
||
self.db = db
|
||
|
||
async def create(self, user_id: str, data: dict) -> Skill:
|
||
skill = Skill(
|
||
owner_id=user_id,
|
||
**data
|
||
)
|
||
self.db.add(skill)
|
||
await self.db.commit()
|
||
await self.db.refresh(skill)
|
||
return skill
|
||
|
||
async def get_by_id(self, skill_id: str) -> Optional[Skill]:
|
||
result = await self.db.execute(select(Skill).where(Skill.id == skill_id))
|
||
return result.scalar_one_or_none()
|
||
|
||
async def list_for_user(
|
||
self,
|
||
user_id: str,
|
||
agent_type: Optional[str] = None,
|
||
visibility: Optional[str] = None,
|
||
) -> list[Skill]:
|
||
"""列出用户可访问的 Skills"""
|
||
conditions = [
|
||
or_(
|
||
Skill.owner_id == user_id,
|
||
Skill.visibility == "market",
|
||
and_(Skill.visibility == "team", Skill.team_id == user_id),
|
||
)
|
||
]
|
||
if agent_type:
|
||
conditions.append(Skill.agent_type == agent_type)
|
||
if visibility:
|
||
conditions.append(Skill.visibility == visibility)
|
||
|
||
result = await self.db.execute(
|
||
select(Skill)
|
||
.where(*conditions)
|
||
.where(Skill.is_active == True)
|
||
.order_by(Skill.created_at.desc())
|
||
)
|
||
return list(result.scalars().all())
|
||
|
||
async def update(self, skill_id: str, user_id: str, data: dict) -> Optional[Skill]:
|
||
skill = await self.get_by_id(skill_id)
|
||
if not skill or skill.owner_id != user_id:
|
||
return None
|
||
for key, value in data.items():
|
||
if value is not None:
|
||
setattr(skill, key, value)
|
||
await self.db.commit()
|
||
await self.db.refresh(skill)
|
||
return skill
|
||
|
||
async def delete(self, skill_id: str, user_id: str) -> bool:
|
||
skill = await self.get_by_id(skill_id)
|
||
if not skill or skill.owner_id != user_id:
|
||
return False
|
||
await self.db.delete(skill)
|
||
await self.db.commit()
|
||
return True
|
||
|
||
async def get_by_agent_type(self, agent_type: str) -> list[Skill]:
|
||
"""获取指定 Agent 类型的所有可用 Skills(供 Agent 运行时加载)"""
|
||
result = await self.db.execute(
|
||
select(Skill)
|
||
.where(
|
||
and_(
|
||
Skill.agent_type == agent_type,
|
||
Skill.is_active == True,
|
||
Skill.visibility.in_(["market", "private"]),
|
||
)
|
||
)
|
||
)
|
||
return list(result.scalars().all())
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add backend/app/services/skill_service.py
|
||
git commit -m "feat: add SkillService"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Skill Router (API Endpoints)
|
||
|
||
**Files:**
|
||
- Create: `backend/app/routers/skill.py`
|
||
- Modify: `backend/app/routers/__init__.py`
|
||
- Modify: `backend/app/main.py`
|
||
|
||
- [ ] **Step 1: Create skill router**
|
||
|
||
```python
|
||
# backend/app/routers/skill.py
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from app.database import get_db
|
||
from app.models.user import User
|
||
from app.routers.auth import get_current_user
|
||
from app.schemas.skill import SkillCreate, SkillUpdate, SkillOut
|
||
from app.services.skill_service import SkillService
|
||
|
||
router = APIRouter(prefix="/api/skills", tags=["Skill"])
|
||
|
||
|
||
@router.post("", response_model=SkillOut, status_code=201)
|
||
async def create_skill(
|
||
data: SkillCreate,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""创建新 Skill"""
|
||
svc = SkillService(db)
|
||
skill = await svc.create(current_user.id, data.model_dump())
|
||
return skill
|
||
|
||
|
||
@router.get("", response_model=list[SkillOut])
|
||
async def list_skills(
|
||
agent_type: str | None = None,
|
||
visibility: str | None = None,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""列出用户可访问的 Skills"""
|
||
svc = SkillService(db)
|
||
skills = await svc.list_for_user(current_user.id, agent_type, visibility)
|
||
return skills
|
||
|
||
|
||
@router.get("/{skill_id}", response_model=SkillOut)
|
||
async def get_skill(
|
||
skill_id: str,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取 Skill 详情"""
|
||
svc = SkillService(db)
|
||
skill = await svc.get_by_id(skill_id)
|
||
if not skill:
|
||
raise HTTPException(status_code=404, detail="Skill not found")
|
||
return skill
|
||
|
||
|
||
@router.put("/{skill_id}", response_model=SkillOut)
|
||
async def update_skill(
|
||
skill_id: str,
|
||
data: SkillUpdate,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""更新 Skill"""
|
||
svc = SkillService(db)
|
||
skill = await svc.update(skill_id, current_user.id, data.model_dump(exclude_unset=True))
|
||
if not skill:
|
||
raise HTTPException(status_code=404, detail="Skill not found or not owned")
|
||
return skill
|
||
|
||
|
||
@router.delete("/{skill_id}", status_code=204)
|
||
async def delete_skill(
|
||
skill_id: str,
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""删除 Skill"""
|
||
svc = SkillService(db)
|
||
deleted = await svc.delete(skill_id, current_user.id)
|
||
if not deleted:
|
||
raise HTTPException(status_code=404, detail="Skill not found or not owned")
|
||
```
|
||
|
||
- [ ] **Step 2: Update routers __init__.py**
|
||
|
||
Add to `backend/app/routers/__init__.py`:
|
||
```python
|
||
from app.routers.skill import router as skill_router
|
||
```
|
||
|
||
- [ ] **Step 3: Register in main.py**
|
||
|
||
Add import and include_router:
|
||
```python
|
||
from app.routers.skill import router as skill_router
|
||
# ...
|
||
app.include_router(skill_router)
|
||
```
|
||
|
||
- [ ] **Step 4: Test API**
|
||
|
||
Run: `cd backend && python -c "from app.main import app; print('Routes:', [r.path for r in app.routes if 'skill' in r.path])"`
|
||
|
||
Expected: Routes containing `/api/skills`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add backend/app/routers/skill.py backend/app/routers/__init__.py backend/app/main.py
|
||
git commit -m "feat: add Skill API endpoints"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: Skill Registry (Agent Integration)
|
||
|
||
**Files:**
|
||
- Create: `backend/app/agents/skill_registry.py`
|
||
- Modify: `backend/app/agents/prompts.py` (extend system prompts)
|
||
|
||
- [ ] **Step 1: Create skill registry**
|
||
|
||
```python
|
||
# backend/app/agents/skill_registry.py
|
||
"""
|
||
Skill Registry - Agent 运行时加载 Skills
|
||
"""
|
||
from typing import Optional
|
||
from app.database import async_session
|
||
from app.services.skill_service import SkillService
|
||
|
||
|
||
# 缓存:agent_type -> list[Skill]
|
||
_skill_cache: dict[str, list] = {}
|
||
|
||
|
||
async def load_skills_for_agent(agent_type: str, force_reload: bool = False) -> list:
|
||
"""加载指定 Agent 类型的可用 Skills"""
|
||
if not force_reload and agent_type in _skill_cache:
|
||
return _skill_cache[agent_type]
|
||
|
||
async with async_session() as db:
|
||
svc = SkillService(db)
|
||
skills = await svc.get_by_agent_type(agent_type)
|
||
_skill_cache[agent_type] = skills
|
||
return skills
|
||
|
||
|
||
def get_skills_for_agent(agent_type: str) -> list:
|
||
"""同步接口:返回缓存的 Skills(供 Agent 节点调用)"""
|
||
return _skill_cache.get(agent_type, [])
|
||
|
||
|
||
def build_skill_context(agent_type: str) -> str:
|
||
"""
|
||
构建 Skill 上下文,供注入到 Agent 系统提示
|
||
格式:Skill 名称 + 描述 + 工具列表
|
||
"""
|
||
skills = get_skills_for_agent(agent_type)
|
||
if not skills:
|
||
return ""
|
||
|
||
lines = ["\n\n【可用的 Skills】"]
|
||
for s in skills:
|
||
tools_str = ", ".join(s.tools) if s.tools else "无"
|
||
lines.append(f"""
|
||
## {s.name}
|
||
- 描述: {s.description or '无'}
|
||
- 工具: {tools_str}
|
||
- 指令: {s.instructions[:200]}...""" if len(s.instructions) > 200 else f"- 指令: {s.instructions}")
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def clear_cache():
|
||
"""清除缓存(配置变更时调用)"""
|
||
global _skill_cache
|
||
_skill_cache = {}
|
||
```
|
||
|
||
- [ ] **Step 2: Update prompts to include skill context**
|
||
|
||
Modify `backend/app/agents/graph.py` to inject skill context into each agent's system prompt:
|
||
|
||
In each agent node function (planner_node, executor_node, etc.), append skill context:
|
||
```python
|
||
skill_context = build_skill_context(agent_type)
|
||
# Append to system message
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add backend/app/agents/skill_registry.py
|
||
git commit -m "feat: add SkillRegistry for agent integration"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: Frontend - Skill API Client
|
||
|
||
**Files:**
|
||
- Create: `frontend/src/api/skill.ts`
|
||
|
||
- [ ] **Step 1: Create skill API client**
|
||
|
||
```typescript
|
||
// frontend/src/api/skill.ts
|
||
import api from './index'
|
||
import type { AxiosResponse } from 'axios'
|
||
|
||
export interface Skill {
|
||
id: string
|
||
name: string
|
||
description: string | null
|
||
instructions: string
|
||
agent_type: string
|
||
tools: string[]
|
||
required_context: string[]
|
||
output_format: string | null
|
||
visibility: 'private' | 'team' | 'market'
|
||
team_id: string | null
|
||
is_active: boolean
|
||
owner_id: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface SkillCreate {
|
||
name: string
|
||
description?: string
|
||
instructions: string
|
||
agent_type: string
|
||
tools?: string[]
|
||
required_context?: string[]
|
||
output_format?: string
|
||
visibility?: 'private' | 'team' | 'market'
|
||
team_id?: string
|
||
is_active?: boolean
|
||
}
|
||
|
||
export interface SkillUpdate {
|
||
name?: string
|
||
description?: string
|
||
instructions?: string
|
||
agent_type?: string
|
||
tools?: string[]
|
||
required_context?: string[]
|
||
output_format?: string
|
||
visibility?: 'private' | 'team' | 'market'
|
||
team_id?: string
|
||
is_active?: boolean
|
||
}
|
||
|
||
export const skillApi = {
|
||
list: (params?: { agent_type?: string; visibility?: string }): Promise<AxiosResponse<Skill[]>> => {
|
||
return api.get('/api/skills', { params })
|
||
},
|
||
|
||
get: (id: string): Promise<AxiosResponse<Skill>> => {
|
||
return api.get(`/api/skills/${id}`)
|
||
},
|
||
|
||
create: (data: SkillCreate): Promise<AxiosResponse<Skill>> => {
|
||
return api.post('/api/skills', data)
|
||
},
|
||
|
||
update: (id: string, data: SkillUpdate): Promise<AxiosResponse<Skill>> => {
|
||
return api.put(`/api/skills/${id}`, data)
|
||
},
|
||
|
||
delete: (id: string): Promise<AxiosResponse<void>> => {
|
||
return api.delete(`/api/skills/${id}`)
|
||
},
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/api/skill.ts
|
||
git commit -m "feat: add skill API client"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: Frontend - SkillView Page
|
||
|
||
**Files:**
|
||
- Create: `frontend/src/views/SkillView.vue`
|
||
|
||
- [ ] **Step 1: Create SkillView page**
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { skillApi, type Skill } from '@/api/skill'
|
||
import { Bot, Plus, Edit2, Trash2, Eye, EyeOff, Copy } from 'lucide-vue-next'
|
||
|
||
const skills = ref<Skill[]>([])
|
||
const loading = ref(false)
|
||
const showModal = ref(false)
|
||
const editingSkill = ref<Skill | null>(null)
|
||
|
||
const form = ref({
|
||
name: '',
|
||
description: '',
|
||
instructions: '',
|
||
agent_type: 'planner',
|
||
tools: [] as string[],
|
||
visibility: 'private' as const,
|
||
is_active: true,
|
||
})
|
||
|
||
const agentTypes = [
|
||
{ value: 'master', label: 'Master' },
|
||
{ value: 'planner', label: 'Planner' },
|
||
{ value: 'executor', label: 'Executor' },
|
||
{ value: 'librarian', label: 'Librarian' },
|
||
{ value: 'analyst', label: 'Analyst' },
|
||
]
|
||
|
||
const availableTools = [
|
||
'search_knowledge', 'hybrid_search', 'get_knowledge_graph_context',
|
||
'build_knowledge_graph', 'get_tasks', 'create_task', 'update_task_status',
|
||
'get_forum_posts', 'create_forum_post', 'scan_forum_for_instructions',
|
||
]
|
||
|
||
onMounted(loadSkills)
|
||
|
||
async function loadSkills() {
|
||
loading.value = true
|
||
try {
|
||
const res = await skillApi.list()
|
||
skills.value = res.data
|
||
} catch (e) {
|
||
console.error('加载失败', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function openCreate() {
|
||
editingSkill.value = null
|
||
form.value = {
|
||
name: '',
|
||
description: '',
|
||
instructions: '',
|
||
agent_type: 'planner',
|
||
tools: [],
|
||
visibility: 'private',
|
||
is_active: true,
|
||
}
|
||
showModal.value = true
|
||
}
|
||
|
||
function openEdit(skill: Skill) {
|
||
editingSkill.value = skill
|
||
form.value = {
|
||
name: skill.name,
|
||
description: skill.description || '',
|
||
instructions: skill.instructions,
|
||
agent_type: skill.agent_type,
|
||
tools: skill.tools || [],
|
||
visibility: skill.visibility,
|
||
is_active: skill.is_active,
|
||
}
|
||
showModal.value = true
|
||
}
|
||
|
||
async function saveSkill() {
|
||
try {
|
||
if (editingSkill.value) {
|
||
await skillApi.update(editingSkill.value.id, form.value)
|
||
} else {
|
||
await skillApi.create(form.value)
|
||
}
|
||
showModal.value = false
|
||
await loadSkills()
|
||
} catch (e) {
|
||
console.error('保存失败', e)
|
||
}
|
||
}
|
||
|
||
async function toggleActive(skill: Skill) {
|
||
try {
|
||
await skillApi.update(skill.id, { is_active: !skill.is_active })
|
||
await loadSkills()
|
||
} catch (e) {
|
||
console.error('更新失败', e)
|
||
}
|
||
}
|
||
|
||
async function deleteSkill(skill: Skill) {
|
||
if (!confirm(`确定删除 Skill "${skill.name}"?`)) return
|
||
try {
|
||
await skillApi.delete(skill.id)
|
||
await loadSkills()
|
||
} catch (e) {
|
||
console.error('删除失败', e)
|
||
}
|
||
}
|
||
|
||
function copySkill(skill: Skill) {
|
||
form.value = {
|
||
name: skill.name + '_copy',
|
||
description: skill.description || '',
|
||
instructions: skill.instructions,
|
||
agent_type: skill.agent_type,
|
||
tools: skill.tools || [],
|
||
visibility: 'private',
|
||
is_active: true,
|
||
}
|
||
editingSkill.value = null
|
||
showModal.value = true
|
||
}
|
||
|
||
function getAgentLabel(type: string) {
|
||
return agentTypes.find(a => a.value === type)?.label || type
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="skill-view">
|
||
<div class="view-header">
|
||
<h2>Skill 市场</h2>
|
||
<button class="primary-btn" @click="openCreate">
|
||
<Plus :size="14" /> 新建 Skill
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="loading" class="loading">加载中...</div>
|
||
|
||
<div v-else class="skill-list">
|
||
<div v-for="skill in skills" :key="skill.id" class="skill-card">
|
||
<div class="skill-header">
|
||
<div class="skill-name">
|
||
<Bot :size="14" />
|
||
{{ skill.name }}
|
||
</div>
|
||
<div class="skill-badge" :class="skill.agent_type">
|
||
{{ getAgentLabel(skill.agent_type) }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="skill-desc">{{ skill.description || '无描述' }}</div>
|
||
|
||
<div class="skill-tools">
|
||
<span v-for="tool in skill.tools" :key="tool" class="tool-tag">
|
||
{{ tool }}
|
||
</span>
|
||
<span v-if="!skill.tools?.length" class="tool-tag empty">无工具</span>
|
||
</div>
|
||
|
||
<div class="skill-footer">
|
||
<span class="skill-visibility">{{ skill.visibility }}</span>
|
||
<div class="skill-actions">
|
||
<button class="icon-btn" @click="toggleActive(skill)" :title="skill.is_active ? '禁用' : '启用'">
|
||
<component :is="skill.is_active ? Eye : EyeOff" :size="14" />
|
||
</button>
|
||
<button class="icon-btn" @click="copySkill(skill)" title="复制">
|
||
<Copy :size="14" />
|
||
</button>
|
||
<button class="icon-btn" @click="openEdit(skill)" title="编辑">
|
||
<Edit2 :size="14" />
|
||
</button>
|
||
<button class="icon-btn danger" @click="deleteSkill(skill)" title="删除">
|
||
<Trash2 :size="14" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="skills.length === 0" class="empty-state">
|
||
暂无 Skill,点击上方按钮创建
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal -->
|
||
<div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
|
||
<div class="modal">
|
||
<h3>{{ editingSkill ? '编辑 Skill' : '新建 Skill' }}</h3>
|
||
|
||
<div class="form-group">
|
||
<label>名称</label>
|
||
<input v-model="form.name" placeholder="Skill 名称" />
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>描述</label>
|
||
<input v-model="form.description" placeholder="供 LLM 理解的描述" />
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>适用 Agent</label>
|
||
<select v-model="form.agent_type">
|
||
<option v-for="a in agentTypes" :key="a.value" :value="a.value">
|
||
{{ a.label }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>引用工具</label>
|
||
<div class="tools-grid">
|
||
<label v-for="tool in availableTools" :key="tool" class="checkbox-item">
|
||
<input type="checkbox" :value="tool" v-model="form.tools" />
|
||
{{ tool }}
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>指令模板</label>
|
||
<textarea v-model="form.instructions" rows="6" placeholder="Agent 执行时的指令..."></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>可见性</label>
|
||
<select v-model="form.visibility">
|
||
<option value="private">私有</option>
|
||
<option value="team">团队共享</option>
|
||
<option value="market">市场</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button @click="showModal = false">取消</button>
|
||
<button class="primary-btn" @click="saveSkill">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.skill-view {
|
||
padding: 24px;
|
||
}
|
||
|
||
.view-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.view-header h2 {
|
||
font-size: 18px;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.primary-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 16px;
|
||
background: var(--accent-cyan-dim);
|
||
border: 1px solid var(--border-mid);
|
||
border-radius: var(--radius-md);
|
||
color: var(--accent-cyan);
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.skill-list {
|
||
display: grid;
|
||
gap: 16px;
|
||
}
|
||
|
||
.skill-card {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border-dim);
|
||
border-radius: var(--radius-lg);
|
||
padding: 16px;
|
||
}
|
||
|
||
.skill-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.skill-name {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.skill-badge {
|
||
font-size: 10px;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
background: var(--accent-cyan-dim);
|
||
color: var(--accent-cyan);
|
||
}
|
||
|
||
.skill-badge.master { background: rgba(249,168,37,0.1); color: var(--accent-amber); }
|
||
.skill-badge.executor { background: rgba(255,71,87,0.1); color: var(--accent-red); }
|
||
|
||
.skill-desc {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.skill-tools {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.tool-tag {
|
||
font-size: 10px;
|
||
padding: 2px 8px;
|
||
background: rgba(0,245,212,0.05);
|
||
border: 1px solid var(--border-dim);
|
||
border-radius: 4px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.skill-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.skill-visibility {
|
||
font-size: 10px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.skill-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
|
||
.icon-btn {
|
||
padding: 4px;
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.icon-btn:hover {
|
||
background: var(--bg-panel);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.icon-btn.danger:hover {
|
||
background: rgba(255,71,87,0.1);
|
||
color: var(--accent-red);
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
color: var(--text-dim);
|
||
padding: 40px;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
color: var(--text-dim);
|
||
padding: 40px;
|
||
}
|
||
|
||
/* Modal styles */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.6);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
}
|
||
|
||
.modal {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border-mid);
|
||
border-radius: var(--radius-lg);
|
||
padding: 24px;
|
||
width: 500px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal h3 {
|
||
margin-bottom: 20px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
background: var(--bg-panel);
|
||
border: 1px solid var(--border-dim);
|
||
border-radius: var(--radius-md);
|
||
color: var(--text-primary);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.tools-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.checkbox-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
margin-top: 20px;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/views/SkillView.vue
|
||
git commit -m "feat: add SkillView page"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: Frontend - Router & Navigation
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/router/index.ts`
|
||
- Modify: `frontend/src/components/SidebarNav.vue`
|
||
|
||
- [ ] **Step 1: Add route to router**
|
||
|
||
In `frontend/src/router/index.ts`, add to children array:
|
||
```typescript
|
||
{
|
||
path: 'skills',
|
||
name: 'skills',
|
||
component: () => import('@/views/SkillView.vue'),
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 2: Add nav item to SidebarNav**
|
||
|
||
In `frontend/src/components/SidebarNav.vue`, add to navItems array:
|
||
```typescript
|
||
{ name: 'Skill 市场', path: '/skills', icon: Bot },
|
||
```
|
||
|
||
Also add Bot to the import from lucide-vue-next.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add frontend/src/router/index.ts frontend/src/components/SidebarNav.vue
|
||
git commit -m "feat: add Skill route and navigation"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: Integration - Inject Skill Context into Agent
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/agents/graph.py`
|
||
|
||
- [ ] **Step 1: Modify agent nodes to include skill context**
|
||
|
||
In each agent node function, after creating the system message, append skill context:
|
||
|
||
```python
|
||
from app.agents.skill_registry import build_skill_context
|
||
|
||
async def planner_node(state: AgentState) -> AgentState:
|
||
# ... existing code ...
|
||
system_msgs: list[BaseMessage] = [SystemMessage(content=PLANNER_SYSTEM_PROMPT)]
|
||
|
||
# Inject skill context
|
||
skill_ctx = build_skill_context("planner")
|
||
if skill_ctx:
|
||
system_msgs.append(SystemMessage(content=skill_ctx))
|
||
|
||
# ... rest of code ...
|
||
```
|
||
|
||
Apply same pattern to: master_node, executor_node, librarian_node, analyst_node
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add backend/app/agents/graph.py
|
||
git commit -m "feat: inject skill context into agent prompts"
|
||
```
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
| Task | Description | Files |
|
||
|------|-------------|-------|
|
||
| 1 | Skill Model | `backend/app/models/skill.py` |
|
||
| 2 | Skill Schema | `backend/app/schemas/skill.py` |
|
||
| 3 | Skill Service | `backend/app/services/skill_service.py` |
|
||
| 4 | Skill Router | `backend/app/routers/skill.py`, `main.py` |
|
||
| 5 | Skill Registry | `backend/app/agents/skill_registry.py` |
|
||
| 6 | Frontend API | `frontend/src/api/skill.ts` |
|
||
| 7 | SkillView Page | `frontend/src/views/SkillView.vue` |
|
||
| 8 | Router & Nav | `frontend/src/router/index.ts`, `SidebarNav.vue` |
|
||
| 9 | Agent Integration | `backend/app/agents/graph.py` |
|
||
|
||
---
|
||
|
||
## Verification
|
||
|
||
1. **Backend API Test:**
|
||
- Start backend: `cd backend && python -m uvicorn app.main:app --reload`
|
||
- Test: `curl -X POST http://localhost:8000/api/skills -H "Content-Type: application/json" -d '{"name":"test","instructions":"test","agent_type":"planner"}'`
|
||
|
||
2. **Frontend Test:**
|
||
- Start frontend: `cd frontend && npm run dev`
|
||
- Navigate to `/skills`, verify page loads
|
||
|
||
3. **Agent Integration Test:**
|
||
- Create a Skill via API
|
||
- Send message to chat that triggers the skill's agent type
|
||
- Verify skill context appears in agent logs
|