backend/app/SKILL.md
# SKILL 后端功能说明文档 ## 概述 SKILL(技能)模块是平台的核心功能之一,用于管理和组织可复用的技能资源。每个技能可以包含描述、内容、标签、文件等丰富的信息,支持公开分享和私有管理。 ## 架构设计 ### 分层架构 SKILL 模块采用经典的分层架构设计: ``` API Layer (api/v1/skills.py) ↓ Service Layer (services/skill_service.py) ↓ Repository Layer (repositories/skill.py) ↓ Model Layer (models/skill.py) ``` ### 核心组件 1. **模型层 (Models)** - `Skill`: 技能主表模型 - `SkillFile`: 技能文件关联表模型 2. **仓库层 (Repositories)** - `SkillRepository`: 技能数据访问层 - `SkillFileRepository`: 技能文件数据访问层 3. **服务层 (
npx skillsauth add jd-opensource/joysafeter backend/appInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
SKILL(技能)模块是平台的核心功能之一,用于管理和组织可复用的技能资源。每个技能可以包含描述、内容、标签、文件等丰富的信息,支持公开分享和私有管理。
SKILL 模块采用经典的分层架构设计:
API Layer (api/v1/skills.py)
↓
Service Layer (services/skill_service.py)
↓
Repository Layer (repositories/skill.py)
↓
Model Layer (models/skill.py)
模型层 (Models)
Skill: 技能主表模型SkillFile: 技能文件关联表模型仓库层 (Repositories)
SkillRepository: 技能数据访问层SkillFileRepository: 技能文件数据访问层服务层 (Services)
SkillService: 技能业务逻辑和权限校验API层 (API)
技能主表,存储技能的核心信息:
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键,自动生成 |
| name | String(255) | 技能名称,必填 |
| description | Text | 技能描述,必填 |
| content | Text | 技能内容,必填 |
| tags | JSONB | 标签列表,默认为空列表 |
| source_type | String(50) | 来源类型,默认为 "local" |
| source_url | String(1024) | 来源 URL,可选 |
| root_path | String(512) | 根路径,可选 |
| owner_id | String(255) | 拥有者 ID,外键关联 user.id |
| created_by_id | String(255) | 创建者 ID,外键关联 user.id,必填 |
| is_public | Boolean | 是否公开,默认为 False |
| license | String(100) | 许可证信息,可选 |
| created_at | DateTime | 创建时间 |
| updated_at | DateTime | 更新时间 |
约束和索引:
(owner_id, name) - 同一拥有者的技能名称必须唯一skills_owner_idx: 拥有者索引skills_created_by_idx: 创建者索引skills_public_idx: 公开状态索引skills_tags_idx: 标签 GIN 索引(支持 JSONB 查询)技能文件关联表,存储技能关联的文件信息:
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID | 主键,自动生成 |
| skill_id | UUID | 技能 ID,外键关联 skills.id |
| path | String(512) | 文件路径,必填 |
| file_name | String(255) | 文件名,必填 |
| file_type | String(50) | 文件类型,必填 |
| content | Text | 文件内容,可选 |
| storage_type | String(20) | 存储类型,默认为 "database" |
| storage_key | String(512) | 存储键(如对象存储的 key),可选 |
| size | Integer | 文件大小(字节),默认为 0 |
| created_at | DateTime | 创建时间 |
| updated_at | DateTime | 更新时间 |
索引:
skill_files_skill_idx: 技能 ID 索引skill_files_path_idx: 技能 ID + 路径复合索引功能描述: 获取技能列表,支持按用户、公开状态、标签过滤。
权限控制:
查询参数:
include_public (bool): 是否包含公开技能,默认 Truetags (List[str]): 标签过滤,支持多标签筛选实现位置:
GET /v1/skillsSkillService.list_skills()SkillRepository.list_by_user()查询逻辑:
user_id 存在且 include_public=True:返回 owner_id == user_id 或 is_public == True 或 owner_id == None(系统级公共技能)user_id 存在且 include_public=False:只返回 owner_id == user_id 的技能user_id 不存在且 include_public=True:返回所有公开技能user_id 不存在且 include_public=False:不返回任何结果tags,使用 JSONB 的 contains 操作符进行标签过滤功能描述: 根据技能 ID 获取技能详情,包含关联的文件列表。
权限控制:
实现位置:
GET /v1/skills/{skill_id}SkillService.get_skill()SkillRepository.get_with_files()权限检查逻辑:
if skill.owner_id and skill.owner_id != current_user_id and not skill.is_public:
raise ForbiddenException("You don't have permission to access this skill")
功能描述: 创建新的技能,支持同时创建关联的文件。
权限要求: 需要登录
实现位置:
POST /v1/skillsSkillService.create_skill()业务规则:
owner_id,则使用 created_by_id 作为拥有者files 参数传入,每个文件包含:
path: 文件路径file_name: 文件名file_type: 文件类型content: 文件内容(可选)storage_type: 存储类型(默认 "database")storage_key: 存储键(可选)size: 文件大小(默认 0)请求体示例:
{
"name": "Python 数据分析",
"description": "使用 Python 进行数据分析的技能",
"content": "详细的技能内容...",
"tags": ["python", "data-analysis"],
"source_type": "local",
"is_public": false,
"files": [
{
"path": "/examples",
"file_name": "example.py",
"file_type": "python",
"content": "print('Hello World')",
"storage_type": "database",
"size": 20
}
]
}
功能描述: 更新已存在的技能信息。
权限要求: 只有拥有者可以更新自己的技能
实现位置:
PUT /v1/skills/{skill_id}SkillService.update_skill()业务规则:
可更新字段:
name: 技能名称description: 技能描述content: 技能内容tags: 标签列表source_type: 来源类型source_url: 来源 URLroot_path: 根路径owner_id: 拥有者 IDis_public: 是否公开license: 许可证功能描述: 删除技能及其关联的所有文件。
权限要求: 只有拥有者可以删除自己的技能
实现位置:
DELETE /v1/skills/{skill_id}SkillService.delete_skill()业务规则:
SkillFileRepository.delete_by_skill() 显式删除文件记录功能描述: 向已存在的技能添加文件。
权限要求: 只有拥有者可以向自己的技能添加文件
实现位置:
POST /v1/skills/{skill_id}/filesSkillService.add_file()业务规则:
功能描述: 删除技能关联的特定文件。
权限要求: 只有拥有者可以删除自己技能的文件
实现位置:
DELETE /v1/skills/files/{file_id}SkillService.delete_file()业务规则:
skill_id 找到对应的技能,检查拥有者权限| 操作 | 拥有者 | 其他用户 | 未登录用户 | |------|--------|----------|------------| | 查看自己的技能 | ✅ | ❌ | ❌ | | 查看公开技能 | ✅ | ✅ | ✅ | | 创建技能 | ✅ | ❌ | ❌ | | 更新自己的技能 | ✅ | ❌ | ❌ | | 删除自己的技能 | ✅ | ❌ | ❌ | | 添加文件 | ✅ | ❌ | ❌ | | 删除文件 | ✅ | ❌ | ❌ |
所有权限检查都在 SkillService 层实现:
get_skill() 方法中检查update_skill() 方法中检查delete_skill() 方法中检查add_file() 和 delete_file() 方法中检查提供技能数据访问方法:
list_by_user(): 根据用户 ID、公开状态、标签查询技能列表get_with_files(): 获取技能及其关联的文件(使用 selectinload 预加载)count_by_user(): 统计用户拥有的技能数量get_by_name_and_owner(): 根据名称和拥有者查询技能(用于唯一性检查)提供技能文件数据访问方法:
list_by_skill(): 获取技能的所有文件delete_by_skill(): 删除技能的所有文件(批量删除)所有技能相关的 API 端点都在 /v1/skills 路径下。
| 方法 | 路径 | 说明 | 需要认证 |
|------|------|------|----------|
| GET | /v1/skills | 获取技能列表 | 可选 |
| POST | /v1/skills | 创建技能 | ✅ |
| GET | /v1/skills/{skill_id} | 获取技能详情 | 可选 |
| PUT | /v1/skills/{skill_id} | 更新技能 | ✅ |
| DELETE | /v1/skills/{skill_id} | 删除技能 | ✅ |
| POST | /v1/skills/{skill_id}/files | 添加文件 | ✅ |
| DELETE | /v1/skills/files/{file_id} | 删除文件 | ✅ |
所有 API 响应都遵循统一的格式:
成功响应:
{
"success": true,
"data": { ... }
}
错误响应:
{
"success": false,
"error": "错误信息"
}
技能模块使用以下自定义异常:
NotFoundException: 资源不存在(如技能或文件不存在)ForbiddenException: 权限不足(如非拥有者尝试修改技能)BadRequestException: 请求参数错误(如同名技能已存在)所有异常都在 SkillService 层抛出,由 API 层的全局异常处理器统一处理。
Skill → AuthUser (owner)
owner_id → user.idondelete="SET NULL": 用户删除时,拥有者设为 NULLSkill → AuthUser (created_by)
created_by_id → user.idondelete="CASCADE": 用户删除时,删除其创建的所有技能SkillFile → Skill
skill_id → skills.idondelete="CASCADE": 技能删除时,级联删除所有关联文件Skill.owner: lazy="selectin" - 使用 selectin 加载Skill.created_by: lazy="selectin" - 使用 selectin 加载Skill.files: lazy="selectin" - 使用 selectin 加载,支持级联删除SkillFile.skill: lazy="selectin" - 使用 selectin 加载from app.services.skill_service import SkillService
service = SkillService(db)
skill = await service.create_skill(
created_by_id="user123",
name="Python 爬虫",
description="使用 Python 进行网页爬取的技能",
content="详细的技能内容...",
tags=["python", "web-scraping"],
is_public=True,
files=[
{
"path": "/examples",
"file_name": "scraper.py",
"file_type": "python",
"content": "import requests\n...",
"storage_type": "database",
"size": 1024
}
]
)
# 获取当前用户的所有技能(包括公开的)
skills = await service.list_skills(
current_user_id="user123",
include_public=True,
tags=["python"]
)
skill = await service.update_skill(
skill_id=skill.id,
current_user_id="user123",
description="更新后的描述",
is_public=False
)
selectinload 策略,避免 N+1 查询问题app/models/skill.pyapp/repositories/skill.pyapp/services/skill_service.pyapp/api/v1/skills.pyalembic/versions/development
Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas
development
Use when you have a spec or requirements for a multi-step task, before touching code
testing
OpenClaw Skills 全方位安全审计工具,检测供应链投毒、Prompt注入、恶意代码模式、权限越权和依赖风险
tools
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends an agent's capabilities with specialized knowledge, workflows, or tool integrations.