Files
JARVIS/docs/superpowers/plans/2026-03-21-skill-system-implementation.md

1118 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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