- AI Agent
- 人工智能
- 代码智能体
- 交互助手
【免费下载链接】openchamber
Agentic Development Environment based on OpenCode AI agent
本篇文章以 OpenChamber 仓库中packages/web/server/lib/skills-catalog/DOCUMENTATION.md为核心,结合模块源码与路由实现,系统性讲解 Skills Catalog 模块如何实现"基于 git 仓库的技能(Skill)发现、扫描与安装"。读完本文,你将掌握源字符串解析规则、扫描与安装的完整调用链、三层缓存架构、冲突解决策略以及安全边界,并可直接据此理解或扩展该模块。
模块定位:OpenChamber 中的技能分发中枢
在 OpenChamber(基于 OpenCode AI Agent 的 Agentic Development Environment)中,技能(Skill)是赋予 Agent 特定能力的可复用单元。Skills Catalog 模块位于packages/web/server/lib/skills-catalog/,承担三类核心职责:
- 发现(Discovery):维护一份预置技能源列表,并在 Web 界面上展示每个源仓库的 Star 数、最近推送时间等元信息;
- 扫描(Scanning):克隆远程 git 仓库,解析其中的
SKILL.md文件,将每个技能目录整理为结构化的候选清单; - 安装(Installation):按用户或项目作用域,将选中的技能目录复制到 OpenCode/Agent 约定的技能目录中,并处理目标目录已存在时的冲突。
从 技能路由注册文件 可以看到,模块的 8 个导出函数(getCuratedSkillsSources、getCacheKey、scanWithCache、parseSkillRepoSource、scanSkillsRepository、installSkillsFromRepository、fetchGitHubRepoMetas等)被统一注入到路由依赖中,通过/api/config/skills/catalog、/api/config/skills/scan、/api/config/skills/install等 HTTP 接口对外提供服务。
模块文件结构
| 文件 | 职责 |
|---|---|
| cache.js | 扫描结果的内存缓存 + TTL + 磁盘持久化 + 并发控制 |
| curated-sources.js | 预置技能源常量与访问函数 |
| github-meta.js | GitHub 仓库元信息(Star、最近推送)的尽力而为抓取 |
| git.js | git 命令封装、认证错误识别、git 可用性检查 |
| install.js | 从 git 仓库安装技能 |
| scan.js | 从 git 仓库扫描技能 |
| source.js | 技能源字符串解析 |
| disk-cache.js | 磁盘缓存读写(原子写入) |
技能源字符串解析:三种格式的统一入口
parseSkillRepoSource(source, options)是模块的"门面",负责把用户输入的各种源字符串统一为结构化对象。从 source.js 源码看,它支持三种格式:
1. HTTPS URL 格式
https://host/owner/repo(.git),解析后同时生成 SSH 与 HTTPS 两种克隆地址:
// 输入: https://github.com/anthropics/skills.git // 输出: { ok: true, host: 'github.com', owner: 'anthropics', repo: 'skills', cloneUrlSsh: 'git@github.com:anthropics/skills.git', cloneUrlHttps: 'https://github.com/anthropics/skills.git', effectiveSubpath: null, normalizedRepo: 'anthropics/skills' }2. SSH URL 格式
git@host:owner/repo(.git),其子路径只能通过options.subpath传入(源码注释明确说明 "For SSH URLs, subpath is only accepted via options.subpath")。
3. 简写格式(Shorthand)
owner/repo[/subpath...],这是预置源与配置中最常用的形式。子路径既可以直接拼在字符串末尾,也可以通过options.subpath显式传入,最终以显式参数优先:
// 输入: 'anthropics/skills/skills' // effectiveSubpath = 'skills'(来自字符串) // 输入: 'anthropics/skills' + { subpath: 'skills' } // effectiveSubpath = 'skills'(来自 options)解析失败时统一返回{ ok: false, error: { kind: 'invalidSource', message } },例如空字符串、缺少 owner/repo、无法识别的格式等。
预置技能源
curated-sources.js 中定义了 4 个预置源(CURATED_SKILLS_SOURCES),getCuratedSkillsSources()返回其副本:
| id | label | source | defaultSubpath |
|---|---|---|---|
anthropic | Anthropic | anthropics/skills | skills |
openai | OpenAI | openai/skills | skills/.curated |
cursor | Cursor | cursor/plugins | pstack/skills |
mattpocock | Matt Pocock | mattpocock/skills | (无,扫描整个仓库) |
除预置源外,技能目录路由 还会从磁盘设置中读取settings.skillCatalogs,将其中的自定义条目(含id、label、source、subpath、gitIdentityId)合并进目录列表,最终在/api/config/skills/catalog接口中一并返回给前端。
扫描流水线:从克隆到 SKILL.md 结构化
scanSkillsRepository({ source, subpath, defaultSubpath, identity })是扫描入口,完整流程如下(对应 scan.js):
- 前置检查:
assertGitAvailable()确认 git 在 PATH 中可用,不可用则直接返回gitUnavailable错误。 - 源解析:调用
parseSkillRepoSource;effectiveSubpath的优先级为parsed.effectiveSubpath → defaultSubpath。 - 克隆策略:根据
identity?.sshKey是否存在决定使用 SSH 还是 HTTPS 克隆地址。克隆采用--depth=1 --filter=blob:none --no-checkout的部分克隆(partial clone)优先方案,失败后回退到--depth=1(install.js 中克隆超时为 90 秒,scan.js 中为 60 秒)。 - 稀疏检出:执行
sparse-checkout init --no-cone+sparse-checkout set <patterns>+checkout --force HEAD,只检出SKILL.md相关文件。有子路径时 patterns 为${subpath}/SKILL.md与${subpath}/**/SKILL.md,无子路径时覆盖仓库根与任意层级。 - 定位 SKILL.md:优先用
git ls-files列出文件;失败则回退到git ls-tree -r --name-only HEAD。若子路径不存在,视为空扫描直接返回{ ok: true, items: [] }。 - 解析 frontmatter:对每个技能目录读取
SKILL.md,用正则提取---分隔的 YAML frontmatter,通过yaml.parse解析出name与description字段;缺少分隔符或 YAML 解析失败会生成对应 warning。 - 技能名校验:技能名取目录 basename,必须匹配
/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/(1-64 个字符、小写字母数字加连字符);不合法则installable: false并附 warning。 - 并行与排序:最多 10 个 worker 并行解析,最终按
skillName.localeCompare排序保证 UI 展示顺序稳定。 - 清理:
finally块中调用safeRm删除临时克隆目录。
注意一个细节:仓库根目录的SKILL.md会被过滤掉(p !== 'SKILL.md'),源码注释说明这是因为"根级 SKILL.md 无法映射到 OpenCode 的『技能名 == 目录名』约定"。
安装流水线:作用域、冲突解决与稀疏检出
installSkillsFromRepository(...)的参数比扫描更丰富:scope(user/project)、targetSource(opencode/agents)、workingDirectory、userSkillDir、selections、conflictPolicy、conflictDecisions。核心流程(对应 install.js):
目标目录规则
getTargetSkillDir按scope与targetSource组合决定安装位置:
| scope | targetSource | 目标目录 |
|---|---|---|
user | opencode | <userSkillDir>/<skillName> |
user | agents | ~/.agents/skills/<skillName> |
project | opencode | <workingDirectory>/.opencode/skills/<skillName> |
project | agents | <workingDirectory>/.agents/skills/<skillName> |
userSkillDir会先经过normalizeUserSkillDir归一化:若传入的是旧的skill(单数)目录,且旧目录存在而skills(复数)不存在,则沿用旧目录,否则指向~/.config/opencode/skills下的复数目录。
校验与冲突预检
scope只能是user/project,targetSource只能是opencode/agents,project作用域必须携带workingDirectory,否则返回invalidSource;selections为空时直接返回错误;- 安装前先对每个待装技能计算目标目录,若已存在且既无 per-skill 决策、也无自动策略(
skipAll/overwriteAll),则收集为冲突并返回{ kind: 'conflicts', conflicts }——这一步保证在克隆/下载之前就提示用户,避免浪费网络请求。
克隆与选择性检出
克隆策略与扫描一致,随后执行sparse-checkout init --cone+sparse-checkout set <requestedDirs>,只检出用户实际选择的技能目录,是控制克隆体积的关键手段。
逐技能安装与冲突决策
对每个技能:
- 目录名不合法 → 记入
skipped(reason:Invalid skill name (directory basename)); - 检出目录中缺少
SKILL.md→ 记入skipped; - 冲突决策优先级:
conflictDecisions[skillName](per-skill)>conflictPolicy(skipAll跳过 /overwriteAll覆盖)> 无冲突时默认覆盖; - 覆盖前调用
safeRm(targetDir)清空旧目录; - 通过
copyDirectoryNoSymlinks复制文件,复制失败则回滚删除目标目录并记入skipped; - 成功后记入
installed(含{ skillName, scope, source })。
三层缓存:内存、去重、磁盘
扫描与元信息抓取都有成本(克隆仓库、请求 GitHub API),因此模块实现了三层缓存机制,集中在 cache.js 与 github-meta.js:
缓存键与 TTL
- 扫描缓存键由
getCacheKey({ normalizedRepo, subpath, identityId })生成,形如repo::subpath::identity,三个维度隔离不同源、不同子路径、不同 git 身份; - 默认 TTL 为3 小时(
DEFAULT_TTL_MS = 3 * 60 * 60 * 1000),扫描结果与 GitHub 元信息一致; - GitHub 元信息抓取失败时采用更短的 5 分钟失败缓存(
FAILURE_CACHE_TTL_MS),避免频繁重试已失败/限流的 API。
scanWithCache:并发控制核心
scanWithCache(key, loader, { refresh })实现了三级防护:
- 读缓存:
refresh: false(默认)时命中未过期缓存直接返回; - in-flight 去重:同一 key 的并发请求共享同一次 loader 运行(
inFlightMap),避免重复克隆; - 全局并发上限:信号量
acquireScanSlot/releaseScanSlot保证同时最多2 个扫描任务(MAX_CONCURRENT_SCANS = 2),超出者排队等待。
只有ok: true的结果才会写入缓存(setCachedScan内部再次校验 TTL 数值合法性)。
磁盘持久化
- 内存缓存通过 disk-cache.js 持久化到 OpenChamber 数据目录(
OPENCHAMBER_DATA_DIR环境变量或~/.config/openchamber)下的skills-catalog-cache.json与skills-github-meta.json; - 写入采用防抖 + 原子重命名:
setTimeout1000ms 后统一落盘,先写*.tmp临时文件再renameSync原子替换,避免并发写坏文件; - 重启后
loadDiskEntries会过滤掉已过期的条目重新载入内存,因此应用重启与页面刷新都能复用历史扫描结果,而不是重新访问 GitHub; - 写入失败被静默忽略,内存缓存保持权威,下次成功写入会重试持久化。
元信息抓取的"尽力而为"原则
fetchGitHubRepoMetas(normalizedRepos)通过 GitHub REST API(https://api.github.com/repos/<owner>/<repo>)抓取stargazers_count与pushed_at,映射为{ stars, repoUpdatedAt }。源码注释强调其设计意图:
- 单请求超时1500ms,严格低于目录接口的客户端请求期限,确保"可选的元信息增强永远不会拖垮目录加载";
- 失败 resolve 为
null,同一仓库的并发请求去重,失败结果短时缓存(5 分钟)。
在 技能目录路由 中,目录接口只对host === 'github.com'的源发起元信息抓取,并把stars、repoUpdatedAt附加到每个源对象上供前端展示。
HTTP 接口与响应契约
路由层 skill-routes.js 把模块能力暴露为以下接口:
| 接口 | 方法 | 说明 |
|---|---|---|
/api/config/skills/catalog | GET | 返回技能源目录(含自定义源与 GitHub 元信息) |
/api/config/skills/catalog/source?sourceId= | GET | 按源 ID 扫描并返回技能清单,refresh=true强制绕过缓存 |
/api/config/skills/scan | POST | 对任意源字符串执行扫描 |
/api/config/skills/install | POST | 安装所选技能 |
/api/config/skills/:name | GET | 读取单个技能详情 |
各层统一采用{ ok, ... }结果对象而非抛异常,错误分类保持一致:
authRequired:认证失败(SSH/HTTPS),接口返回 401,并附带identities列表供前端引导用户配置 git 身份;networkError:克隆等网络操作失败;conflicts:目标目录已存在且无自动决策,接口返回409;invalidSource:源字符串或参数不合法,返回 400;unknown:其他未知错误,返回 500。
扫描响应(Scan Response)
{ ok: true, normalizedRepo: 'anthropics/skills', // owner/repo effectiveSubpath: 'skills', // 实际生效的子路径 items: [{ repoSource: 'anthropics/skills', // 原始源字符串 repoSubpath: 'skills', // 子路径 skillDir: 'skills/foo', // 仓库内目录(POSIX) skillName: 'foo', // 目录 basename frontmatterName: 'Foo Skill', // SKILL.md frontmatter 的 name description: '...', // frontmatter 的 description installable: true, // 是否可通过命名校验 warnings: undefined // 解析警告(如有) }] }安装响应(Install Response)
{ ok: true, installed: [{ skillName: 'foo', scope: 'user', source: 'opencode' }], skipped: [{ skillName: 'bar', reason: 'Invalid skill name (directory basename)' }] }安装成功后路由层会进一步返回requiresReload与message字段(安装成功时提示"Skills installed successfully.",全部跳过时提示 "No skills were installed"),驱动前端刷新技能状态。
安全与健壮性设计
模块在安全方面有明确设计,文档与源码相互印证:
- 路径穿越防护:
copyDirectoryNoSymlinks先realpath解析源目录,复制过程中对每个子目录再次realpath并校验其位于源目录之内(startsWith(srcReal)),越界即抛错; - 拒绝符号链接:复制时
lstat检查到SymbolicLink直接抛Symlinks are not supported in skills,防止技能通过软链逃逸出技能目录; - 非交互式 git:
runGit始终注入GIT_TERMINAL_PROMPT=0,防止克隆私有仓库时因交互式密码提示而挂起;注入 SSH 身份时使用ssh -i <key> -o BatchMode=yes -o StrictHostKeyChecking=accept-new,避免主机密钥交互; - 临时目录兜底清理:扫描与安装都在
finally中safeRm临时目录;安装失败时还会回滚已创建的目标目录; - 认证错误识别:
looksLikeAuthError通过正则(permission denied、publickey、could not read from remote repository、authentication failed等)识别认证失败,从而给用户更友好的提示; - 磁盘缓存文件权限:写盘时使用
mode: 0o600,避免缓存文件被其他用户读取。
扩展与贡献指引
若要在该模块中新增一类技能源(例如公司内部 git 服务),文档给出了清晰的五步流程:
- 在
packages/web/server/lib/skills-catalog/下新建子目录(如newsource/); - 实现
scan.js,导出返回{ ok, items, error? }(符合 SkillsCatalogItem 契约)的函数; - 实现
install.js,导出接受 selections 并返回{ ok, installed, skipped, error? }的函数; - 如需出现在默认目录中,将新源加入
curated-sources.js的CURATED_SKILLS_SOURCES; - 在
packages/web/server/index.js中 import 并接线新源。
提交前建议运行验证命令(仓库根目录执行):
bun run type-check # 类型检查 bun run lint # 静态检查 bun run build # 构建同时注意文档提醒的边界情况:不存在的仓库、无认证的私有仓库、缺失 SKILL.md、非法技能名、冲突与网络失败等,这些在 cache.test.js、github-meta.test.js、skill-routes.test.js 等测试文件中均有覆盖,是理解模块行为边界的最佳参考。
小结
OpenChamber 的 Skills Catalog 模块是一个"小而精"的工程范例:通过统一的源解析、高效的稀疏检出克隆策略、三层缓存与并发控制,把"从 git 仓库安装技能"这一高频操作做得既快又稳;同时通过严格的结果对象契约、分类错误与安全校验,保证 Web 服务层的健壮性。理解它的扫描/安装流水线与缓存设计,不仅有助于使用和扩展 OpenChamber 的技能体系,也能为同类"远程内容 → 本地可执行资产"的分发类模块提供可复用的设计参考。
- AI Agent
- 人工智能
- 代码智能体
- 交互助手
【免费下载链接】openchamber
Agentic Development Environment based on OpenCode AI agent
相关推荐
OpenChamber 1.3.9 版本解析:Skills 技能管理与技能目录(Skills Catalog)能力上线
OpenChamber 1.3.9 版本解析:Skills 技能管理与技能目录(Skills Catalog)能力上线 本文基于 changelog/1.3.9
AI Agent人工智能代码智能体交互助手Agent Skills 的发现、验证与安装实战:Meshery 仓库 find-skills 技能全解析
Agent Skills 的发现、验证与安装实战:Meshery 仓库 find skills 技能全解析 导读 本文以 Meshery 仓库内建技能 .age
云原生微服务运维DevOps如何永久保存你的QQ空间青春记忆:GetQzonehistory工具终极指南
如何永久保存你的QQ空间青春记忆:GetQzonehistory工具终极指南 你是否曾经想要找回多年前在QQ空间发布的心情说说,却发现部分内容已经消失不见?Ge
网页爬虫数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考