Agent Zero 技能管理 API 实战解析:api/skills.py 的 list / delete 契约、过滤机制与源码实现
【免费下载链接】agent-zeroAgent Zero AI framework项目地址: https://gitcode.com/GitHub_Trending/ag/agent-zero
本文以 api/skills.py.dox.md 为纲,结合 api/skills.py 的运行时实现与底层助手层 helpers/skills.py,完整拆解 Agent Zero 中/skills端点的请求/响应契约、按项目与 Agent 画像过滤的底层原理、删除动作的安全护栏,以及从 Web UI 前端到测试用例的完整调用链。读完后,你可以独立维护该端点(修改 payload、新增过滤维度或排查删除失败),并理解其鉴权、CSRF 与文件系统副作用的边界。
一、端点定位与职责划分
DOX 文档(api/skills.py.dox.md)开宗明义:该模块拥有skillsAPI 端点,职责是列出并管理可用于设置界面和 Agent 技能流程的可用技能。它同时声明了一个重要的目录约定——api/目录被有意保持扁平(flat),因此每个端点文件旁都配有一个*.dox.md档案,并且必须与源码保持同步。
职责划分(Ownership)在 DOX 中写得很明确:
- api/skills.py 拥有运行时实现;
- api/skills.py.dox.md 拥有关于职责、契约、副作用和验证方式的持久化说明。
涉及的类是Skills,继承自ApiHandler,共暴露三个方法:
class Skills(ApiHandler): async def process(self, input: Input, request: Request) -> Output def list_skills(self, input: Input) def delete_skill(self, input: Input)这是api/目录下大量端点的统一模式:每个<name>.py文件里放一个ApiHandler子类,DOX 档案则把契约从代码中"提炼"出来,便于维护者在不通读全部源码的情况下快速对齐接口约定。
端点是如何被注册和分发的
从源码结构看,Skills并不是在某个地方显式注册的。helpers/api.py 中的register_api_route把整条/api/<path>通配路由交给一个动态分发器_dispatch:
- 按路径拼出
api/<path>.py(先查内置api/目录,plugins/<plugin>/...前缀则查插件目录),用load_classes_from_file加载其中的ApiHandler子类; - 校验 HTTP 方法是否允许(
get_methods()默认只允许POST); - 依据类方法声明层层包裹安全装饰器(CSRF、API key、登录鉴权、回环限制),并缓存包装后的 handler;
- 由
watchdog监听api/与usr/api/目录,文件变化时清空缓存热加载。
因此该端点对外的 URL 就是POST /api/skills,而 api/skills.py 只需专注业务逻辑,路由、安全与热重载全部由框架兜底。
二、安全模型:默认开启登录鉴权与 CSRF
Skills没有覆写任何类方法声明,因此完全继承 helpers/api.py 中ApiHandler的默认值:
| 声明 | 默认值 | 含义 |
|---|---|---|
requires_auth() | True | 需要有效登录会话(session 校验) |
requires_csrf() | requires_auth() | 鉴权开启则 CSRF 必须通过(X-CSRF-Token头或 cookie) |
get_methods() | ["POST"] | 仅接受 POST |
requires_api_key() | False | 不要求X-API-KEY |
requires_loopback() | False | 不限制仅回环地址 |
DOX 的"Work Guidance"部分也呼应了这一点:除非端点契约明确变更,否则必须保留鉴权、CSRF、回环和 API-key 检查。由于delete动作会真实删除文件系统目录,这套默认防护不是可选项——CSRF 失败会直接返回 403,未登录请求会被重定向到登录页。
三、请求/响应契约:两种 action,一种响应外壳
process(api/skills.py#L6-L25)是唯一的入口,按input["action"]分派:
async def process(self, input: Input, request: Request) -> Output: action = input.get("action", "") try: if action == "list": data = self.list_skills(input) elif action == "delete": data = self.delete_skill(input) else: raise Exception("Invalid action") return {"ok": True, "data": data} except Exception as e: return {"ok": False, "error": str(e)}请求参数一览:
| action | 参数 | 必填 | 说明 |
|---|---|---|---|
list | project_name | 否 | 字符串,去除首尾空白后参与按项目过滤 |
list | agent_profile | 否 | 字符串,去除首尾空白后参与按 Agent 画像过滤 |
delete | skill_path | 是 | 技能目录路径,去除空白后不能为空 |
响应外壳统一为两种形态:
{ "ok": true, "data": { } } { "ok": false, "error": "Invalid action" }两个实现细节值得注意:
- 错误不抛 500。
process内部自行捕获所有异常并返回ok: false的字典(HTTP 仍是 200);只有process之外的框架层异常才会走 helpers/api.py 的 500 分支。前端因此只需判断result.ok即可。 - 未知 action 也是受控错误:任何非
list/delete的取值都会得到{"ok": false, "error": "Invalid action"},契约是封闭的。
list成功时data是技能数组,每项仅含三个字段:name、description、path(路径已转为字符串);delete成功时data为{"ok": true, "skill_path": ...}。
四、list_skills:全局扫描加两级过滤
4.1 端点层的过滤逻辑
list_skills 的流程是"先取全量,再按需过滤":
def list_skills(self, input: Input): skill_list = skills.list_skills() # 1. 全局技能扫描 # 2. 按项目过滤 if project_name := (input.get("project_name") or "").strip() or None: project_folder = projects.get_project_folder(project_name) if runtime.is_development(): project_folder = files.normalize_a0_path(project_folder) skill_list = [ s for s in skill_list if files.is_in_dir(str(s.path), project_folder) ] # 3. 按 Agent 画像过滤 if agent_profile := (input.get("agent_profile") or "").strip() or None: roots = [ files.get_abs_path("agents", agent_profile, "skills"), files.get_abs_path("usr", "agents", agent_profile, "skills"), ] if project_name: roots.append( projects.get_project_meta(project_name, "agents", agent_profile, "skills") ) skill_list = [ s for s in skill_list if any(files.is_in_dir(str(s.path), r) for r in roots) ] # 4. 组装输出并排序 result = [{"name": s.name, "description": s.description, "path": str(s.path)} for s in skill_list] result.sort(key=lambda x: (x["name"], x["path"])) return result几个关键点:
- 过滤是累加的:同时传
project_name和agent_profile时,先按项目目录过滤,再在结果中保留落在"该画像技能根目录"之内的条目;画像的根目录集合在存在项目时会追加一条项目级路径(projects.get_project_meta(project_name, "agents", agent_profile, "skills")),形成"项目内画像"的第三层查找位置。 - 开发模式的特殊处理:
runtime.is_development()为真时对project_folder调用files.normalize_a0_path,把开发环境下的仓库路径归一化,保证is_in_dir前缀匹配不因路径形态差异而失效。 - 输出稳定有序:结果按
(name, path)字典序排序,同一技能名在不同目录下也不会乱序,前端可直接渲染。
4.2 底层扫描:技能根目录、SKILL.md 与 frontmatter 校验
端点只是薄封装,真正的工作在 helpers/skills.py:
- 技能根目录发现:get_skill_roots 在无 Agent 上下文(即本端点的全局调用)时,收集仓库根
skills/、用户区usr/skills/、各项目的usr/projects/*/.a0proj/skills与agents/*/skills、各 Agent 的agents/*/skills与usr/agents/*/skills,以及插件目录plugins/*/skills、usr/plugins/*/skills、插件内 Agent 的plugins/*/agents/*/skills等八类位置(含usr/变体)。这解释了为什么"删除内置插件技能会被拒绝"而用户区技能可以管理——两者都在根目录清单里,但删除守卫对内置区另有拦截(见第五节)。 - SKILL.md 发现:discover_skill_md_files 在每个根目录下递归查找
SKILL.md,跳过任何含隐藏路径段(.开头)的文件,并按路径排序保证确定性。 - frontmatter 解析与容错:split_frontmatter 要求 YAML frontmatter 从文件顶部开始且闭合;优先用 PyYAML 解析,缺失时退化为一个最小 YAML 子集解析器(支持
key: value与- item列表)。skill_from_markdown 还做了跨平台字段别名兼容:name/skill、description/when_to_use/summary、triggers/trigger_patterns/activation、allowed-tools/allowed_tools/tools。 - 校验规则:validate_skill 强制
name匹配^[a-z0-9-]+$、长度 1–64、不能以连字符开头/结尾、不能含连续连字符;description必填且 ≤1024 字符;compatibility≤500 字符。校验不通过的 SKILL.md 会被跳过并输出一次性警告(用_WARNED_SKILL_PARSE_PATHS去重),警告形如skill broken-skill skipped: invalid frontmatter at line 4: Unterminated YAML frontmatter——tests/test_skills_runtime.py#L420-L441 精确断言了这条警告文本与"同一路径只警告一次"的行为。 - 去重策略:list_skills 在全局(无 Agent)场景下不去重,同一技能出现在多个根目录会各计一次;带 Agent 上下文时按归一化名去重,根目录顺序靠前的胜出。本端点调用的是全局模式,因此返回结果中可能存在同名技能的不同路径实例,
path字段正是为消歧而保留的。
五、delete_skill:五道护栏才允许删目录
端点层(api/skills.py#L66-L72)只做了参数非空检查,真正的安全逻辑全部下沉到 helpers/skills.py 的 delete_skill。删除操作按顺序经过五道守卫,任何一道失败都会抛异常并被process转成ok: false:
- 路径归一化:
files.get_abs_path(skill_path)转绝对路径;开发模式下再经files.fix_dev_path修正路径分隔符;最后files.normalize_a0_path统一形态。 - 内置插件保护:归一化路径中出现
/plugins/且不含/usr/plugins/时,直接抛PermissionError("Built-in plugin skills cannot be deleted")。这发生在任何文件系统写入之前——tests/test_skills_runtime.py#L407-L409 的测试名test_builtin_plugin_skill_delete_is_rejected_before_filesystem_delete正是对这一保证的断言。 - 根目录范围检查:路径必须落在 get_skill_roots() 返回的某个技能根之内,否则抛
ValueError("Skill root not in current scope")。这意味着不能借这个端点删除技能树之外的任意目录。 - 目录存在性检查:
os.path.isdir(skill_path)不成立则抛FileNotFoundError("Skill directory not found")。 - 整目录删除:全部通过后调用
files.delete_dir(skill_path)递归删除整个技能目录(技能 = 目录 + SKILL.md 的粒度)。
用户区(usr/前缀)的技能与自定义技能都在合法根目录内,可正常删除;内置plugins/区则被第二道守卫永久保护,与 docs/guides/skills.md 所述"技能可在设置中管理"的用户心智一致。
六、前端调用链:Web UI 设置面板如何消费该端点
DOX 要求"payload 变更时同步更新前端调用方、插件调用方和测试"。当前前端调用方是 webui/components/settings/skills/skills-list-store.js,一个 Alpine store,完整示范了契约的用法:
- 加载列表(loadSkills):
const response = await fetchApi("/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "list", project_name: this.projectName || null, }), }); // result.ok ? result.data : 展示 result.error- 删除技能(deleteSkill):发送
{ action: "delete", skill_path: skill.path },成功后弹 toast 并重新loadSkills()刷新列表;失败则展示后端error原文。 - 打开技能目录(openSkill):
skill.path还会被直接交给文件浏览器(file browser store)打开,供用户查看技能内容——这也是list响应中保留完整路径字段的原因。 - 客户端搜索:关键字过滤(
matchesSearchQuery)在前端本地对name/description/path等字段做包含匹配,服务端只负责项目/画像两级过滤。职责切分清晰:服务端管"可见范围",客户端管"检索体验"。
七、维护契约与验证清单(继承 DOX 的 Work Guidance)
DOX 沉淀的四条维护纪律,对照源码均能落地验证:
- 保留安全检查:修改 api/skills.py 时不要覆写
requires_auth/requires_csrf等类方法,除非契约明确变更;delete的文件系统副作用(delete_dir)尤其依赖这套防护。 - 同步更新所有调用方:payload 形状变更时,需同时改 webui/components/settings/skills/skills-list-store.js 等前端调用方、插件调用方与测试。
- 非 JSON 响应用
helpers.api.Response:ApiHandler.handle_request 对Response实例与 dict 走不同分支,只有需要文件、重定向或特殊状态码时才使用。 - 变更后运行验证:DOX 列出了源码搜索识别到的相关测试,其中与本端点直接相关的是 tests/test_skills_runtime.py——它用 stub 模块隔离加载
helpers/skills.py,覆盖了删除守卫(PermissionError先于文件系统操作抛出)、frontmatter 解析告警、内置 SKILL.md 的合法性回归(如 skills/a0-manage-plugin/SKILL.md)、以及技能检索排序等行为。无聚焦测试时,DOX 建议对浏览器调用方做冒烟验证。
此外,DOX 明确要求:凡请求 payload、鉴权/CSRF 要求、响应形状、路由副作用或 WebSocket 事件契约发生变化,都必须同步更新该 DOX 档案本身——这是api/目录扁平化设计能长期可维护的关键约定。
八、周边端点与延伸阅读
/skills并非孤立端点,api/目录还有一组围绕技能包的姊妹端点,它们复用同一底层助手:
- api/skills_scan.py:扫描已安装技能或上传的
.zip技能包(extract_skills_zip+discover_skill_md_files); - api/skills_import.py 与 api/skills_import_preview.py:将外部技能包导入到
usr/skills/<namespace>/...,导入前先经预览端点确认内容。
面向用户侧的使用说明(聊天输入中的技能选择器、Pin 技能等)见 docs/guides/skills.md;技能作者规范见 docs/developer/contributing-skills.md。理解这些端点与本文剖析的/skills端点共享同一套helpers/skills.py底座(根目录发现、frontmatter 解析、校验与删除守卫),可以形成对 Agent Zero 技能体系的完整认知:/skills负责日常"列出与管理",scan/import 系列负责"引入新技能",而安全边界由根目录清单与内置插件保护两条主线统一兜底。
【免费下载链接】agent-zeroAgent Zero AI framework项目地址: https://gitcode.com/GitHub_Trending/ag/agent-zero
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考