本节摘要:驾驶舱导览第六站——插件架的第一层:Skills(技能)。Skill 是给 agent 的「操作手册」:一份带 frontmatter 的 Markdown 指令模板(可附带资源文件),告诉 agent 在某类任务下按什么流程、用什么约定干活。本节讲清三件事:公共技能目录来自
@openhands/extensions0.19.0 的构建期注入(打包进 Canvas 静态资源,而非运行时加载);src/utils/skill-enablement.ts用「允许清单+拒绝清单」两本账管理哪些技能实际生效;skills-service与 skills 页面(约 2170 行)如何把「目录技能+用户技能+项目技能」三路来源合成一个浏览/启停界面,并落到.agents/skills/目录规范。
内容来源:src/api/skills-service.ts、src/utils/skill-enablement.ts、src/utils/skill-scope.ts、src/types/settings.ts、.agents/skills/release.md、src/components/features/skills/。
⚠️ 注意:Skills 与 MCP 工具的区别先立个标尺——Skill 改变 agent「知道怎么做」(提示词层面的流程知识),MCP 改变 agent「手上有什么」(可调用的工具接口)。Canvas Extensions 则改变 App 本身。三套体系分工的完整对照放在本章第 3 节。
.agents/skills/ 目录规范。skill-enablement.ts 的双清单模型与一次性迁移。skills-service 的三路来源合并(本地 agent-server/bundled 目录/云)。先看本仓库自带的一个真实 skill——.agents/skills/release.md 的开头:
--- name: release description: Guide the release process for @openhands/agent-canvas — review the release-please draft PR, mark it ready, merge it; the tag push publishes to npm and Docker. triggers: - release - new release - cut a release - publish release - bump version --- # Release Process for @openhands/agent-canvas ## Overview Releases are trunk-based and automated by release-please, via the shared reusable workflows in OpenHands/release-actions: ...
解剖出 skill 的三要素:frontmatter 元数据(name 标识;description 让模型判断「当下该不该翻这份手册」;triggers 列出触发短语——用户消息命中即相关);指令正文(结构化的操作流程,agent 执行时作为上下文注入);可选资源(同目录下的脚本、模板等辅助文件)。skill 本质是「按需加载的提示词工程资产」:不占常驻上下文窗口,命中任务才进场。
目录规范沿 .agents/skills/ 约定(src/utils/skill-scope.ts 的注释还保留了历史沿革:/.openhands/microagents/ 是改名前的技能目录,SDK 至今兼容加载,漏判会把存量技能错归为「public」)。同一份代码识别三种 scope:项目技能(仓库内 .agents/skills/)、个人技能(用户家目录 ~/.agents/skills/)、公共技能(目录包发布)——getSkillScope() 按 source 路径的正则特征归类。
@openhands/extensions 0.19.0公共技能从哪来?src/api/skills-service.ts 的注释写得斩钉截铁:
// src/api/skills-service.ts(节选) /** * Public skills loaded from the `@openhands/extensions` npm package. * * This is an **immutable build-time snapshot**: the catalog is baked into * the bundle at `npm run build` / `vite build` time and does not change at * runtime. Updating the catalog requires bumping the `@openhands/extensions` * dependency and rebuilding. */ const PUBLIC_SKILLS: SkillInfo[] = SKILLS_CATALOG.map(catalogEntryToSkillInfo); class SkillsService { static async getSkills(projectDir?: string): Promise<SkillInfo[]> { if (getActiveBackend().backend.kind === "cloud") { return fetchCloudSkills(); } // 公共技能来自打包进来的 @openhands/extensions——不需要 agent-server // 往返,也不需要 GitHub 拉取。只向 agent-server 要用户与项目技能, // 这样本地 .agents/skills/ 内容仍会被拾取。 let localSkills: SkillInfo[] = []; try { const response = await new SkillsClient( getAgentServerClientOptions(), ).getSkills({ load_public: false, // 公共的已随包带来,别重复拉 load_user: true, load_project: true, load_org: false, project_dir: projectDir ?? getAgentServerWorkingDir(), }); localSkills = (response.skills ?? []) as SkillInfo[]; } catch { // agent-server 可能不支持该端点或不可达;回落到仅打包目录 } return [...localSkills, ...PUBLIC_SKILLS]; } }
三个决策值得咀嚼。其一,构建期注入而非运行时加载:SKILLS_CATALOG 是 import 进来的常量,vite build 时静态打进产物——离线可用、无供应链运行时风险、版本与 Canvas 严格对齐;代价是更新技能必须升级依赖重新构建(package.json 里钉着 "@openhands/extensions": "0.19.0")。这与第 4 章 ACP 注册表「上游镜像」、第 5 章 AUTOMATION_CATALOG 是同一发行模式的三个租户:一个 npm 包,三套目录。其二,load_public: false:既然公共目录已随包内置,agent-server 那份就不必再拉,避免重复。其三,失败静默回落:老 agent-server 不支持 skills 端点也不影响页面可用——bundled 目录兜底。云后端则整体换轨到 fetchCloudSkills()。
目录条目还带 compatibility 字段(透传到 SkillInfo.compatibility),标注技能面向的 agent 类型——有些技能描述的流程只在 OpenHands 后端下成立(比如依赖 OpenHands 特有工具的技能),对 ACP 后端(Claude Code 等)无意义,UI 据此提示「该技能对当前后端不适用」。这正是「哪些 skill 对哪个后端类型可用」的把关位置:目录声明兼容性,前端如实呈现。
技能默认开还是关?谁说了算?src/utils/skill-enablement.ts 用两本持久化清单回答,先看数据模型的自白:
// src/utils/skill-enablement.ts(节选) /** * 两个持久化清单覆盖不同人群:enabledSkills 是捆绑目录的允许清单 * ——否则目录每新增一个技能都会默认对所有人开启(#16302); * disabledSkills 继续拒绝用户与项目自写的技能——它们一出现就该是开的。 */ export interface SkillEnablement { enabledSkills?: string[]; // undefined = 尚未迁移 disabledSkills?: string[]; }
这段注释浓缩了一次真实的演进:老模型只有拒绝清单(全部默认开,关掉的记账);问题是目录更新加新技能时,新面孔会自动全开——对一个以「提示词注入」为本质的资产来说,静默上新等于静默改 agent 行为。新模型改成:目录技能进允许清单(不勾不开),本地自写技能仍默认开、进拒绝清单。判定函数一行见真章:
// src/utils/skill-enablement.ts(节选) export function buildSkillEnablementFilter( enablement: SkillEnablement, ): (skillName: string) => boolean { const enabled = new Set(resolveEnabledCatalogSkills(enablement)); const disabled = new Set(enablement.disabledSkills ?? []); return (skillName) => { if (disabled.has(skillName)) return false; // 拒绝清单永远赢 return !isCatalogSkill(skillName) || enabled.has(skillName); // └ 本地技能默认开;目录技能须在允许清单 }; }
存量用户怎么过渡?migrateSkillEnablement() 做一次性转换:enabledSkills 已有值(含全新工作区——显式持久化清单才能挡住未来的默认开启)就返回 undefined 表示无需迁移;否则把老拒绝清单换算成新允许清单,并把已迁入允许清单的名字从拒绝清单里清掉(否则会否决用户日后再打开它)。另外两个小工具也值得一看:findInvokedCatalogSkill() 只看消息首个 token 是否为斜杠命令——24 个目录斜杠命令里 18 个属于默认关闭的技能,若在正文任意位置匹配 /word,自动化卡片会把命令发出去而背后没有指令支撑;isRecommendedSkill() 则暴露目录的默认推荐集(迁移时的兜底值)。
src/components/features/skills/(约 2170 行)是一个完整的「技能商店」界面。组件分工:skills-toolbar+skill-filter/skill-facet-rail/skill-filters-modal 提供搜索与多面筛选(类别、scope、来源);skill-card 渲染卡片,skill-card-pill-row+build-skill-pills 拼装徽标(触发词、类别、兼容性);skill-type-badge/skill-icon-badge 处理类型与图标;skill-detail-modal 展示技能全文——其中「Use skill」按钮把 /<skill-name> 命令插进输入框,与上一节 findAutomationCommand 填充自动化卡片的路径同源;add-skill-modal 引导创建新技能;extensions-navigation/extensions-mobile-hub 则把它与 MCP、Canvas Extensions 拼成统一的「Customize」导航(第 6-03 节详述)。启停开关最终写入 settings 的 enabled_skills/disabled_skills(toSkillEnablement() 完成 snake_case 设置到 camelCase 内部模型的转换)。
合上本节,把技能的供给链画全:
SkillsService.getSkills() ├── 云后端 → fetchCloudSkills()(云侧另有会话级 fetchCloudConversationSkills) └── 本地后端 → SkillsClient.getSkills({load_user,load_project}) │ (agent-server 读 ~/.agents/skills/ 与项目 .agents/skills/) └── 恒定部分 → PUBLIC_SKILLS = SKILLS_CATALOG(@openhands/extensions 构建期打包,不可变快照) ↓ buildSkillEnablementFilter 过滤 UI 渲染 + scope 分组(project / personal / public)
取舍清单:构建期快照换来离线与版本对齐,牺牲即时更新;允许清单换来「新增不默认开启」的安全默认,牺牲一点初始配置成本;三路合并换来「公共+私有」一站式管理,牺牲的是必须处理三处的去重与兼容性标注。每一项都是「提示词资产」这个特殊对象的性质决定的——它是会改变 agent 行为的代码级资产,理应享受接近代码的治理强度。
💡 驾驶舱要点:Skills 体系的核心洞见是「技能即需要治理的提示词资产」——所以它像依赖一样被钉版本、构建期打包;像权限一样被允许/拒绝清单管理;像代码一样做一次性迁移。
@openhands/extensions一个包同时发行 Skills 目录、自动化推荐清单与 MCP 集成市场,是贯穿本章第 1、2 节与第 5 章的同一根发行管线。
.agents/skills/(兼容旧 .openhands/microagents/)。@openhands/extensions 0.19.0 的构建期注入(immutable build-time snapshot):离线可用、版本对齐,更新需 bump 依赖重建;同一包还发行自动化与 MCP 集成目录。skills-service 三路合并:本地 agent-server 取 user/project 技能(load_public: false 防重复),bundled 目录兜底,云后端换轨 fetchCloudSkills;目录条目带 compatibility 标注对 agent 后端类型的适用性。skill-enablement.ts 双清单:目录技能按允许清单(新增不默认开,#16302),本地技能默认开、按拒绝清单关;拒绝清单永远优先;migrateSkillEnablement 一次性迁移存量。findInvokedCatalogSkill 只认首 token 防误触发。下一节:
02 MCP 设置页与工具接入——从「教 agent 做事」转向「给 agent 发工具」。