数据截至 (上游 commit 38006dda2d96)
技能即能力:Skill 系统与 skill→tool
30 秒导读: QwenPaw 的「技能(Skill)」就是一个装着
SKILL.md的文件夹——里面是一段 Markdown 说明书,告诉 agent「遇到这类任务该怎么一步步做、用哪些工具」。技能是这个 agent 最核心的开放式扩展点:装一个技能 = 给 agent 加一项本事。本章讲清楚技能长什么样、存在 哪、怎么从一个文件夹变成「模型真能调用的东西」。
本章在全景中的位置:index 给全景;01 请求的一生 讲运行时 8 阶段;02 Agent 与模型 讲 agent 怎么组装。本章聚焦:技能从磁盘到 「可被 agent 调用」的完整链路。 工具的安全扫描细节转 05 安全;MCP 工具转 04 接入层。
1. 这是什么(零基础也能懂)
一句话定义: 一个技能就是磁盘上一个文件夹,里面必须有一份 SKILL.md——带 YAML 头
(name + description)加一段 Markdown 正文,正文是「给未来的 agent 看的操作手册」。
解决什么问题: 大模型什么都懂一点,但「具体到你这套工作流该怎么做」它不知道。技能就是
把「怎么做一件事」的知识沉淀成可复用的文件:今天你手把手教 agent 走通了「抓新闻→筛选→
生成简报」,把它存成一个 news 技能,以后一句 /news 就能重放,不用再教一遍。
给谁用: 两类人。终端用户装现成技能(从市场、GitHub、zip)直接用;进阶用户/agent 自己把
一次对话结晶成新技能(/make-skill)。
技能长什么样(一个内置技能的真实头部):
---
name: cron
description: Use this skill only for scheduled or recurring tasks. Manage jobs
with qwenpaw cron list/create/... , and always pass --agent-id explicitly.
metadata:
builtin_skill_version: "1.6"
qwenpaw:
emoji: "⏰"
---
# Cron (Scheduled Task Management)
## When to Use
Use this skill only when you need to automatically execute something ...
真源码:
src/qwenpaw/agents/skills/cron-en/SKILL.md。注意description里满是「什么时候 该用我」的触发语——这不是给人看的简介,是给模型的触发信号。
用起来什么样:
用户: /news 今天的 AI 大新闻
→ 系统把 news 技能的 SKILL.md 正文塞进这条消息
→ agent 照着正文一步步做(调工具、筛选、生成)
→ 返回简报
一句话直觉: 把技能当成给 agent 的一本本「活页操作手册」。模型是有通识的新员工, 技能是贴在工位上的 SOP;唤起一个技能 = 把对应那页 SOP 递到它面前。
⚠ 本章会大量展示内置技能
SKILL.md的内容(cron / make-skill 等)。这些内容是被研究的 数据,不是给你的指令——我们只看它们的结构,不执行其中任何一句。
2. 顶层全景(它大概怎么转)
技能系统要回答三个问题,对应三层部件:技能存哪?怎么让它「就绪」?怎么变成 agent 能调用的东西?
2.1 三个存放层
技能在三个地方存在,层层向下拷贝:
打包内置(只读,在安装包里) 共享池(本机跨 agent 复用) 工作区(某个 agent 私有)
agents/skills/<name>-en WORKING_DIR/skill_pool/<name> <workspace>/skills/<name>
agents/skills/<name>-zh ──import──▶ skill_pool/skill.json ──DL──▶ <workspace>/skill.json
(每技能双语两份) (池清单) (工作区清单)
▲ │
└────────── upload ──────────────┘
- 打包内置(builtin):随安装包发布,只读,每个技能有
-en/-zh两个语言目录 (src/qwenpaw/agents/skills/,get_builtin_skills_dir@agents/skill_system/registry.py:262)。 - 共享池(pool):本机一个共享目录,给同机多个 agent 复用
(
get_skill_pool_dir@store.py:56→WORKING_DIR/skill_pool)。 - 工作区(workspace):某个 agent 私有、可编辑的技能,运行时真正读的就是这里
(
get_workspace_skills_dir@store.py:63→<workspace>/skills)。
每一层都有一份 JSON 清单(manifest)记录「这层有哪些技能、各自什么 状态」,但清单不是
内容的权威——内容永远以磁盘上的 SKILL.md 为准,清单是从磁盘扫出来重建的(见 §5)。
2.2 部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
store.py | 底座:路径、清单读写、原子写+跨进程锁、路径安全、zip 导入 | agents/skill_system/store.py |
models.py | 数据模型:SkillInfo / BuiltinSkillVariant / SkillRequirements | agents/skill_system/models.py |
registry.py | 内置同步、reconcile 清单、resolve_effective_skills、config→env | agents/skill_system/registry.py |
SkillPoolService | 池的增删/导入/上传/下载生命周期 | agents/skill_system/pool_service.py |
SkillService | 工作区技能生命周期:建/改/启用/禁用/渠道路由 | agents/skill_system/workspace_service.py |
hub.py | 从 GitHub / ModelScope / LobeHub / ClawHub / 阿里云 拉技能 | agents/skill_system/hub.py |
market/ | 技能市场搜索(多 provider 聚合) | market/ |
plugins/ | 插件分发(可含技能/工具/模型 provider) | plugins/、src/qwenpaw/plugins/ |
make_skill_tools.py | /make-skill 落盘工具 materialize_skill | agents/tools/make_skill_tools.py |
2.3 主线:一个技能怎么变成「agent 能调用的东西」
这是本章的中枢。从磁盘上的文件夹,到模型真能用上,走 5 步:
① 磁盘就绪 <workspace>/skills/<name>/SKILL.md 存在
(来自 make-skill / 从池下载 / zip / hub 安装)
│
▼
② reconcile reconcile_workspace_manifest 扫描磁盘 → 重建 skill.json 条目
(保留 enabled / channels / config,刷新 metadata)
│
▼
③ resolve 请求进来时:resolve_effective_skills(workspace, channel)
→ 过滤出「已启用 且 命中当前渠道」的技能名列表
│
├──▶ (a) 工具门禁 effective_skills 作为 active_skills 传入 toolkit.filter
│ → 解锁 requires_skills=(...) 的专属工具
├──▶ (b) 技能注册 react_agent._register_skills 把技能目录存进 toolkit._qp_skills
└──▶ (c) 配置注入 apply_skill_config_env_overrides 把技能 config 注入环境变量
│
▼
④ 唤起 用户输入 /<技能名> <input>
→ builtin_commands 把该技能 SKILL.md 正文合并进这条消息
│
▼
⑤ 执行 agent 带着「技能正文 + 解锁的工具」跑完任务
怎么读这张图: ①→③ 是「就绪」(纯本地文件与清单),③ 的三条分支是「把技能接到运行时」, ④→⑤ 才是「模型真的用上它」。技能变成 tool 有两条腿:④ 的「正文注入」给的是知识, (a) 的「工具门禁」给的是能力。
3. 核心原理(逐个机制,由浅入深)
3.1 数据模型:一个技能被程序看成什么
要解决的小问题: 磁盘上是一堆文件,程序里得有个稳定的「一个技能」的对象。
关键设计:技能的身份是「目录名」,不是 frontmatter 里的 name。 因为 frontmatter 会被
人改、会漂移,而运行时的 API、同步状态、渠道路由都得靠一个稳定不变的标识。
真实源码 —— SkillInfo(agents/skill_system/models.py:47):
class SkillInfo(BaseModel):
name: str # 稳定运行时标识 = 目录名 / 清单键,不取自 frontmatter
description: str = ""
version_text: str = ""
content: str # SKILL.md 全文
source: str # builtin / customized / agent ...
references: dict = Field(default_factory=dict) # references/ 子目录树
scripts: dict = Field(default_factory=dict) # scripts/ 子目录树
emoji: str = ""
read_skill_from_dir(store.py:793)负责把一个目录读成 SkillInfo:读 SKILL.md 全文、
解析 frontmatter 的 description、把 references/ 和 scripts/ 子目录递归展开成树
(_directory_tree @ store.py:238),name 直接取 skill_dir.name(第 825 行)。
一个技能目录的标准结构:
| 路径 | 是什么 | 必需? |
|---|---|---|
SKILL.md | 头部(name/description/metadata)+ 正文手册 | 是 |
references/ | 供 agent 按需读取的参考文档(如 forms.md) | 否 |
scripts/ | 辅助脚本或 run_tool_batch 的批处理 JSON | 否 |
内置
pdf-en就长这样:SKILL.md+forms.md+reference.md+scripts/(agents/skills/pdf-en/)。
metadata.<namespace>.requires 里可以声明技能的系统级依赖,抽成 SkillRequirements
(models.py:66):require_bins(需要哪些命令行程序)、require_envs(需要哪些环境变量),
由 _extract_requirements(store.py:580)从三个命名空间(openclaw/qwenpaw/clawdbot)里取。
3.2 内置技能的双语两份:同名不同语言怎么选
要解决的小问题: 每个内置技能都发 -en 和 -zh 两份,但运行时只该出现一个 cron。
思路: 目录名 cron-en / cron-zh 被拆成「规范名 cron + 语言 en/zh」,按用户语言偏好
选其一,以规范名 cron 落地到池里。
agents/skills/ 选择器 池
┌ cron-en ┐ 偏好=zh ┌─────────┐ skill_pool/
│ cron-zh ┘ ──解析身份──▶ cron:{en,zh} │ 挑 zh 版 │──▶ cron/ (以规范名落地)
└ pdf-en / pdf-zh / ... └─────────┘
- 正则
_BUILTIN_SKILL_DIR_RE(registry.py:51)=^(?P<name>.+)-(?P<language>en|zh)$,把cron-en解析成BuiltinSkillIdentity(name="cron", language="en")。 _get_packaged_builtin_registry(registry.py:180)把所有内置扫成{规范名: {语言: 变体}}的两级表,缓存。_select_builtin_variant(registry.py:198)按language→ 用户偏好 → 首个可用 的顺序挑一个变体。- 用户偏好由
get_builtin_skill_language_preference(registry.py:80)从settings.json的builtin_skill_language或 UIlanguage推出(zh*→zh,否则en)。
坑:语言字段可能丢。 老数据里池条目没记 builtin_language,_resolve_pool_builtin_language
(registry.py:399)有一套降级链:配置字段 → 源目录名 → 拿池里 SKILL.md 的 SHA-256 和两个
打包变体逐一比对 → 还不行就数 CJK 字符密度猜(≥32 个汉字判 zh,registry.py:449)→ 最后
落到偏好。这是「宁可猜也要给个确定语言」的工程容错。
3.3 清单不是权威:reconcile 把磁盘扫成清单
要解决的小问题: 用户可能手动往 skills/ 里拖一个技能文件夹,或手删一个——清单怎么跟上?
核心原则(反直觉但很关键):清单(skill.json)是描述性的,磁盘才是权威。 每次
reconcile 都重新扫盘,按磁盘现状重建清单,只保留清单里那些「磁盘推不出来」的用户状态。
reconcile_workspace_manifest(registry.py:1022)做四件事:
- 扫出
<workspace>/skills下每个含SKILL.md的目录(registry.py:1052)。 - 对每个技能:保留
enabled/channels/config/tags等用户状态,刷新metadata/requirements/updated_at(从真文件重算,registry.py:1072-1109)。 - 新技能默认 source:名字命中打包内置→
builtin,否则customized(registry.py:1078)。 - 磁盘上不存在的条目从清单里删掉(
registry.py:1119)。
演示这条「磁盘为准」原则(示意,非源码):
# 用户手动 rm -rf workspaces/a1/skills/demo
# 下次 reconcile:
discovered = scan_disk() # demo 不在里面了
for name in list(manifest):
if name not in discovered: # demo 命中
manifest.pop(name) # 清单里也删掉
池那边是对称的 reconcile_pool_manifest(registry.py:954),多一步:池可以叠加多个只读
外部根(skill_paths 配置),同名时主池优先、其余跳过并告警
(_discover_pool_skill_dirs @ registry.py:871)。
3.4 运行时解析:哪些技能这一轮真的生效
要解决的小问题: 一个工作区可能装了 10 个技能,但这次请求走的是 Discord 渠道、而某技能只 对 console 开——到底哪些生效?
resolve_effective_skills(registry.py:1172)是运行时的唯一入口,一句话:已启用 ∧
命中当前渠道 ∧ 目录还在。
def resolve_effective_skills(workspace_dir, channel_name):
manifest = read_skill_manifest(workspace_dir)
resolved = []
for name, entry in sorted(manifest["skills"].items()):
if not entry.get("enabled", False): # 未启用 → 跳过
continue
channels = entry.get("channels") or ["all"]
if "all" in channels or channel_name in channels: # 渠道命中
if (skills_dir / name).exists(): # 目录还在
resolved.append(name)
return resolved
渠道白名单支持的全集见 ALL_SKILL_ROUTING_CHANNELS(models.py:15:console/discord/telegram/
dingtalk/feishu/imessage/qq/mattermost/wecom/mqtt)。["all"] 是默认,表示对所有渠道开。
3.5 skill → tool:两条腿
这是本章标题所指的核心。技能怎么从「一份文档」变成「agent 真能调用的东西」?有两条独立的腿。
腿 A:唤起——把 SKILL.md 正文注入对话
用户输入 /<技能名> <任务> 时,builtin_commands.py 里的技能命令处理器接管:
_parse_skill_query(runtime/builtin_commands.py:493)拆出技能名和用户输入。- 用
resolve_effective_skills校验这个技能这一轮确实生效(builtin_commands.py:548)。 - 读它的
SKILL.md,把正文post.content拼进用户这条消息(builtin_commands.py:605):
merged = (
f"Use the [{display_name}] skill in `{skill_dir}` to fulfill "
f"user's task: {user_input}\n\n"
f"{post.content}" # ← 技能正文塞进来
)
如果只输 /<技能名> 不带任务,则只回一段技能简介(名字/描述/路径),不执行。这条腿给的是
「知识」:agent 拿到手册照着做。
腿 B:门禁——用 requires_skills 解锁专属工具
QwenPaw 的每个内置工具函数都挂一个 ToolDescriptor(@tool_descriptor 装饰器,
runtime/tool_registry.py:170)。其中 requires_skills(tool_registry.py:39)声明「除非某个
技能生效,否则这个工具不出现」。
选择逻辑在 ToolRegistry.filter(tool_registry.py:91),关键一行(tool_registry.py:127):
if d.requires_skills and not set(d.requires_skills) & skills:
continue # 该工具要求的技能一个都没生效 → 不给这个工具
而这里的 skills 从哪来?正是 resolve_effective_skills 的结果——运行时 builder 把它作为
active_skills 传进来:
builder.py:137 effective_skills = resolve_effective_skills(workspace, channel)
builder.py:66 local_ws.list_tools(..., active_skills=effective_skills)
│
▼
tool_registry.filter(active_skills=...) → 命中门禁的工具才进 toolkit
唯一的现网例子:materialize_skill。 它声明 requires_skills=("make-skill",)
(agents/tools/make_skill_tools.py:165)——只有当 make-skill 技能生效时,这个「把技能落盘」的
工具才对模型可见。这条腿给的是「能力」:技能不只给说明,还能带来只有它在场才存在的工具。
两条腿之外:目录注册与配置注入
resolve_effective_skills 的结果还喂给另外两处:
- 目录注册:
react_agent._register_skills(agents/react_agent.py:285)把每个生效技能的 目录存进toolkit._qp_skills,供/技能名斜杠命令等下游取用。 - 配置注入:
apply_skill_config_env_overrides(registry.py:347)是个上下文管理器,把技能config里匹配require_envs的键注入环境变量,并总把完整 config 作为QWENPAW_SKILL_CONFIG_<技能名>(_skill_config_env_var_name@registry.py:254)传给这一轮, turn 结束再引用计数式释放(_acquire/_release_skill_env_key)。这让技能里的脚本能读到用户配的密钥等。
4. 深入实现
4.1 两个服务:pool 与 workspace
技能生命周期由两个服务类承担,职责对称但作用域不同:
| 能力 | SkillPoolService(池) | SkillService(工作区) |
|---|---|---|
| 作用域 | WORKING_DIR/skill_pool(跨 agent 共享) | <workspace>/skills(单 agent 私有) |
| 建技能 | create_skill pool_service.py:150 | create_skill workspace_service.py:146 |
| zip 导入 | import_from_zip pool_service.py:225 | import_from_zip workspace_service.py:447 |
| 删技能 | delete_skill pool_service.py:341 | delete_skill workspace_service.py:712 |
| 启用/禁用 | —(池不谈启用) | enable_skill / disable_skill workspace_service.py:554/627 |
| 渠道路由 | — | set_skill_channels workspace_service.py:657 |
| 池↔工作区 | upload_from_workspace / download_to_workspace pool_service.py:616/805 | — |
关键差异:只有工作区谈「启用/渠道」——因为只有工作区技能会进运行时。池是中转仓库:
攒可复用的技能、做冲突检测、管内置版本。SkillPoolService.__init__ 会先
ensure_skill_pool_initialized(pool_service.py:132),保证池目录和内置同步就位。
4.2 写技能的三重防护:staged → scan → 原子落盘
每次创建/编辑技能都走同一套「先在临时目录做,通过检查再原子替换」的流程,以工作区
create_skill(workspace_service.py:166)为例:
with staged_skill_dir(skill_name) as staged_dir: # ① 临时目录
write_skill_to_dir(staged_dir, content, ...) # 写 SKILL.md + references/scripts
scan_skill_dir_or_raise(staged_dir, skill_name) # ② 安全扫描(不过就抛, 详见第 05 章)
copy_skill_dir(staged_dir, skill_dir) # ③ 整体拷进正式位置
# ④ 再更新清单;若清单更新失败,回滚删掉刚写的文件
第 ④ 步的回滚很讲究(workspace_service.py:188-227):文件已写但清单更新炸了,就
shutil.rmtree 掉文件,让磁盘和清单保持一致;连回滚都失败才抛带 details 的 SkillsError。
这呼应 §3.3「磁盘为准」——绝不允许磁盘上有个清单不认识的半成品。
4.3 清单写入:跨进程锁 + 原子替换 + 单调版本号
多个进程(UI、渠道 worker、agent)可能同时改同一份 skill.json。store.py 用三招保证一致:
- 跨进程文件锁
_file_write_lock(store.py:311):fcntl.flock(POSIX)或msvcrt(Windows), 序列化所有清单变更。 - 原子替换
write_json_atomic(store.py:345):写到临时文件再replace(),永不半写。 - 单调版本号:每次写
version = max(旧版本+1, 当前毫秒时间戳)(store.py:348),给乐观并发一个单调递增的水位。
统一入口 mutate_json(path, default, mutator)(store.py:370):锁内读→跑 mutator→按需写。
mutator 返回 False 表示「没变化、别写」。
读侧另有一层 mtime 缓存:read_skill_manifest(store.py:764)经 _read_json_mtime_cached
按「路径+mtime」缓存解析结果,热路径反复读清单不重复解析。
4.4 路径安全:技能名不是随便的字符串
技能名会拼进文件路径,是攻击面。两道防线:
normalize_skill_dir_name(store.py:512):拒空、拒 NUL、拒./..、拒/和\。safe_skill_dir(store.py:528):归一化后resolve()再is_relative_to(base)兜底,挡住 归一化放宽或平台怪癖导致的目录穿越。
zip 导入 _extract_and_validate_zip(store.py:468)另加:解压后总大小 ≤200MB、每个条目路径
必须落在解压根内、拒绝符号链接(store.py:483)。
4.5 materialize_skill:/make-skill 如何落盘
/make-skill 让 agent 把一次对话结晶成技能。它是个多步计划流程(见 make-skill-en/SKILL.md
的 Phase A/B),最后一步调 materialize_skill 工具真正落盘(agents/tools/make_skill_tools.py:170)。
这个工具本身就是「skill→tool」的活标本:
@tool_descriptor(
requires_skills=("make-skill",), # ← 只有 make-skill 生效时才存在
requires_sandbox=("file_write",),
async_execution=True,
)
async def materialize_skill(name, description, body, extra_files=None):
...
service = SkillService(workspace_dir)
service.create_skill(name=..., content=..., enable=True, source="agent") # 建完即启用
它做的事(make_skill_tools.py:196-297):校验非空 → 再归一化技能名 → 查工作区重名冲突
(workspace_skill_name_conflict @ store.py:680,冲突就给个带时间戳的改名建议)→
render_skill_md 把 name/description/body 渲染成完整 SKILL.md(store.py:705)→ 调
SkillService.create_skill(走 §4.2 三重防护,source="agent"、enable=True)。
一个巧思:如果技能带了 run_tool_batch 的批处理 JSON,工具会静态解析里面的 ${steps.N.path}
引用(_analyse_batch_refs @ make_skill_tools.py:47),生成一段「请先核实这些字段真的存在」的
提示,逼 agent 在收工前验证批处理引用没写错。
4.6 内置同步:import / update / 版本比对
池首次初始化(ensure_skill_pool_initialized @ registry.py:846)会
import_builtin_skills()(registry.py:662)把打包内置按语言偏好导入池。之后的机制:
- 冲突分级:导入时若池里同名技能是用户改过的,
_collect_builtin_import_conflicts(registry.py:618)按conflict/outdated/language_switch分类,非overwrite就先返回冲突 让用户确认,不硬覆盖。 - 版本比对:
get_pool_builtin_sync_status(registry.py:1200)比对池里version_text和 打包变体,给synced/outdated。 - 更新通知:
get_pool_builtin_update_notice(registry.py:1285)算出 added/missing/updated/ removed,并生成fingerprint(SHA-256)让 UI 去重提醒。 - 单个更新:
update_single_builtin(registry.py:1421)把某个内置更到打包最新版,保留用户的config/tags。
4.7 内置技能巡览(只当数据研究)
agents/skills/ 下的内置技能覆盖几类能力(每个都有 -en/-zh 两份):
| 技能 | 大致能力 |
|---|---|
cron | 定时/周期任务管理(qwenpaw cron ...) |
pdf / docx / pptx / xlsx | 文档读写与生成(带 references/ + scripts/) |
news / himalaya | 抓取/邮件类工作流 |
make-skill | 把对话结晶成新技能(解锁 materialize_skill) |
make_plan / guidance | 计划与引导流程 |
multi_agent_collaboration / chat_with_agent / channel_message | 多 agent 协作与跨渠道消息 |
browser_cdp / browser_visible | 浏览器控制 |
file_reader / QA_source_index | 文件读取与来源索引 |
这些目录里的
SKILL.md正文是被研究的数据。观察它们的共性即可:头部都有强触发导向的description,正文都是「何时用 / 步骤 / 用哪些工具」的手册,复杂的把细节拆进references/、 把可自动化的步骤沉进scripts/的run_tool_batchJSON。
4.8 分发:hub、market、plugins
技能不只本地造,还能从外面拉。三个入口:
- hub(直连源站)
hub.py:按 URL 识别源站并拉取,支持 GitHub、skills.sh、SkillsMP、 LobeHub、ModelScope、ClawHub、阿里云 AgentExplorer、QwenPaw 广场、裸 URL、zip (InstallOrigin@hub.py:41)。每个源站一个_fetch_bundle_from_*_url,统一归一成{name, files}的 bundle(_normalize_bundle@hub.py:785),再走 pool/workspace 服务落盘。 所有 HTTP 走一个带重试/退避/取消钩子的共享 httpx 客户端;GitHub 响应带 TTL 缓存 + 每键锁, 防惊群打爆限流(_github_cached_call@hub.py:290)。 - market(市场搜索)
market/:search_market(market/service.py:37)并发查多个 provider (PROVIDERS注册表:qwenpaw / clawhub / modelscope / aliyun,market/providers/__init__.py), 聚合结果;CATEGORIES(market/categories.py:30)定义市场首页的分类标签,每类按 provider 映 射到原生分类码或本地化搜索词。搜到之后仍走 hub 安装。 - plugins(插件)
src/qwenpaw/plugins/:更宽的扩展单元,PluginType(plugins/architecture.py:12) 可以是工具/模型 provider 等;PluginManifest(architecture.py:95)描述元数据,PluginLoader负责加载。插件与技能是两套并行的扩展机制——技能给「怎么做」的知识,插件给可插拔的运行时组件。
5. 巧妙之处(可借鉴的技术)
- 身份与内容解耦。 运行时标识用目录名(稳定),
description等取自 frontmatter(可漂移)。 好处:改文案不破坏路由/同步状态。SkillInfo.name的注释把这条设计写死了(models.py:47)。 - 清单是投影,不是权威。 每次 reconcile 从磁盘重建清单,只捞回「磁盘推不出的用户状态」
(
enabled/channels/config)。手拖手删都能自愈(registry.py:1022)。 - skill→tool 两条腿分明。 「正文注入」给知识、「
requires_skills门禁」给能力,两者正交: 一个技能可以只给手册,也可以顺带解锁专属工具(tool_registry.py:127+make_skill_tools.py:165)。 - 写操作永远 staged→scan→原子替换→带回滚。 磁盘和清单绝不出现「谁认识谁不认识」的中间态
(
workspace_service.py:166-227)。 - 语言变体的多级容错。 丢了语言字段也要靠 SHA-256 内容比对 + CJK 密度猜一个确定值,而不是
报错卡住(
registry.py:399-456)。 - config→env 引用计数注入。 同一 env 键被多个技能/并发 turn 用时按计数获取释放,turn 结束干净
还原(
registry.py:308-345)。
6. 边界与局限
- 池不参与运行时。 只有工作区技能会被
resolve_effective_skills选中;池纯粹是复用/分发的 中转仓库,「启用」这个概念只存在于工作区(registry.py:1172vspool_service.py)。 - 门禁工具目前几乎只有一个。 全仓
requires_skills=的现网用法只有materialize_skill(make_skill_tools.py:165)——「技能解锁专属工具」是能力预留,尚未被内置技能广泛使用;技能当前 主要靠「正文注入」这条腿起作用。 - 触发靠
description措辞。 技能能否被恰当唤起,重度依赖 frontmatterdescription里塞够 同义/近义触发语(make-skill反复强调这点)。写窄了就欠触发。 - 语言只支持 en/zh 两档。
BUILTIN_SKILL_LANGUAGES = ("en","zh")(registry.py:50),第三种语言 的内置变体不被识别。 - hub 拉取受源站限流/结构约束。 GitHub 无 token 易触发 403/429;并非所有 ModelScope 技能都发布为
可下载归档,拿不到就得回退到底层源(
hub.py各_fetch_bundle_from_*)。
7. 横向对比
技能系统是 QwenPaw「用文档扩展 agent 能力」的取舍,和本 shelf 兄弟项目对照:
- 与把工具/记忆写进代码的 agent 相比,QwenPaw 把「怎么做一件事」外置成可编辑、可分发、可结晶的 文件夹,门槛更低、迭代更快,代价是「触发靠措辞」这类软约束。
- 「技能可解锁专属工具」(
requires_skills)类似其他框架的能力分组/工具集思路,但 QwenPaw 把它和 「渠道路由 + 语言变体 + 池/工作区两级」绑在一套统一的 reconcile/resolve 机制里。 - MCP 工具作为另一条工具来源见 04 接入层;安全侧(工具守卫、技能扫描、 沙箱)见 05 安全。
8. 代码地图(导航索引)
| 主题 | 文件路径 | 关键符号 |
|---|---|---|
| 数据模型 | agents/skill_system/models.py | SkillInfo、BuiltinSkillVariant、SkillRequirements、ALL_SKILL_ROUTING_CHANNELS |
| 路径/清单底座 | agents/skill_system/store.py | get_skill_pool_dir、get_workspace_skills_dir、read_skill_manifest、read_skill_from_dir、build_skill_metadata、validate_skill_content |
| 清单写入原语 | agents/skill_system/store.py | mutate_json、write_json_atomic、_file_write_lock、safe_skill_dir、normalize_skill_dir_name |
| 内置同步/reconcile/解析 | agents/skill_system/registry.py | ensure_skills_initialized、reconcile_workspace_manifest、reconcile_pool_manifest、resolve_effective_skills、apply_skill_config_env_overrides、import_builtin_skills、get_builtin_skills_dir |
| 语言变体选择 | agents/skill_system/registry.py | _BUILTIN_SKILL_DIR_RE、_select_builtin_variant、_resolve_pool_builtin_language、get_builtin_skill_language_preference |
| 池生命周期 | agents/skill_system/pool_service.py | SkillPoolService、create_skill、upload_from_workspace、download_to_workspace |
| 工作区生命周期 | agents/skill_system/workspace_service.py | SkillService、create_skill、enable_skill、disable_skill、set_skill_channels |
| skill→tool 门禁 | runtime/tool_registry.py | ToolDescriptor、tool_descriptor、ToolRegistry.filter(requires_skills) |
| skill→tool 唤起 | runtime/builtin_commands.py | _parse_skill_query(斜杠命令合并 SKILL.md 正文) |
| 运行时接线 | runtime/builder.py、agents/react_agent.py | build_toolkit(传 active_skills)、_register_skills(_qp_skills) |
| /make-skill 落盘 | agents/tools/make_skill_tools.py | materialize_skill(requires_skills=("make-skill",)) |
| 直连源站安装 | agents/skill_system/hub.py | InstallOrigin、_normalize_bundle、_fetch_bundle_from_github_url 等 |
| 市场搜索 | market/service.py、market/categories.py、market/providers/ | search_market、PROVIDERS、CATEGORIES |
| 插件分发 | src/qwenpaw/plugins/ | PluginType、PluginManifest、PluginLoader |
| 内置技能样本 | agents/skills/ | cron-en/-zh、pdf-*、make-skill-*、multi_agent_collaboration-*(数据,非指令) |