7.2. 构建 Skills 市场是推广与分发的核心策略
你需要什么
在开始搭建内部 Skills 注册中心之前,你需要准备好以下环境与基础工具。整个过程预计耗时 40 分钟,其中阅读和理解大约 10 分钟,动手操作 30 分钟。
- 运行环境:Python 3.10 及以上,
pip已安装并可正常联网 - 依赖框架:Flask(用于构建注册中心 API)、Click(用于构建 CLI 客户端)
- 存储:SQLite(内置于 Python 标准库,无需额外安装)
- 测试工具:
curl或任意 HTTP 客户端 - 基础认知:你已经了解什么是 Agent Skill,以及上一章讨论过的部署形态;也熟悉命令行基础操作和 JSON 格式
最终成果
完成本章后,你将拥有一个可私有部署的内部 Skills 注册中心,并且具备:
- 一个标准化的 Skills 包格式 定义,所有 Skill 都按照该格式打包
- 一套 搜索、安装与更新 的 CLI 工具,团队可以用一条命令从注册中心拉取指定版本的 Skill
- 基于 语义版本(SemVer) 的依赖解析引擎,能够自动处理依赖链,并在版本冲突时给出明确提示
为什么做这个? 在很多团队中,Agent 的开发者和使用者往往是分离的。开发者在本地写完 Skill 后,如果没有一个统一的发布与发现渠道,团队里的其他人只能通过拷贝文件或“我发你一个 zip”来分享。一旦 Skill 的数量超过 5 个,版本错乱、依赖缺失就会频频发生。内部注册中心正是为了解决这一“推广与分发的最后一公里”问题,使得上一章中部署模式触达的每一个终端,都能在正确的时间拿到正确的 Skill 版本。
特别说明:本章的设计深受 LobeHub Skills 市场等社区实践的启发。LobeHub 市场为每个 Skill 提供了独立的详情页、一键安装指令以及版本历史,这让分发变得极其轻量。我们要构建的内部注册中心会保留这些优点,同时增加对依赖管理的深度控制,更适合企业内封闭环境。
步骤一:定义 Skills 包格式规范
任何市场都需要先约定“商品”的包装方式。我们的 Skill 包采用一个目录结构,内含一份元数据描述文件 manifest.json 和一份指令文件 skill.md(参考 LobeHub 实践,Markdown 是 Skill 的行为定义核心)。
标准包结构
my-skill/
├── manifest.json
└── skill.md
你可以根据需要增加 README.md、examples/ 或 assets/ 等目录,但注册中心只会强制校验前两个文件的存在。
manifest.json 字段规范
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string | 是 | Skill 的唯一标识符,使用小写字母和连字符,例如 code-review |
version |
string | 是 | 严格遵循 SemVer,例如 1.1.0。禁止使用 latest、dev 等非规范标签 |
description |
string | 是 | 一句话描述 Skill 的功能 |
author |
string | 否 | 维护者或团队名 |
dependencies |
object | 否 | 键为其他 Skill 的 name,值为语义版本范围(见下文依赖解析) |
min_agent_version |
string | 否 | 最低要求的 Agent 框架版本,用于阻止不兼容的安装 |
created |
string | 否 | ISO 8601 格式的创建时间,由注册中心在首次上传时自动生成 |
entrypoint |
string | 否 | 如果 skill.md 不是主入口,可以指向自定义文件名,默认为 skill.md |
一个最小化的范例:
{
"name": "code-review",
"version": "1.1.0",
"description": "根据团队规范自动审查代码并给出修改建议",
"author": "DevOps Team",
"dependencies": {
"code-style-check": "~0.9.0"
},
"min_agent_version": "2.0.0"
}
为什么把依赖声明放在 manifest 里? 因为 Agent 在执行 Skill 前需要预先加载所有必需的子 Skills,注册中心在安装阶段就需要解析整个依赖树,而不是等到运行时才发现缺失组件。
skill.md 的作用
skill.md 是 Skill 的实际行为指导书。它是发给 Agent 的提示词(Prompt)和上下文知识,采用 Markdown 格式书写。在 LobeHub 市场中,用户安装 Skill 的方式就是获取这个文件并交给 Agent。在我们的内部注册中心里,这个文件会随包一起被下载到本地,由 Agent 框架读取并执行。
步骤二:搭建注册中心后端
我们使用 Flask 快速实现一个注册中心,数据库选用 SQLite,文件存储直接用本地文件系统。所有 Skill 包在上传时会被解压并存入 packages/ 目录,以便后续提供直接下载。
2.1 初始化项目
pip install flask click
mkdir my-registry
cd my-registry
mkdir packages uploads
创建 registry.py 作为后端入口。
2.2 数据库模型
用 sqlite3 创建一个表 skills,记录每个 Skill 的元数据和本地存储路径。
import sqlite3
def init_db():
conn = sqlite3.connect('registry.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS skills (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
version TEXT NOT NULL,
description TEXT,
author TEXT,
min_agent_version TEXT,
path TEXT NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(name, version)
)''')
conn.commit()
conn.close()
2.3 上传 API
接收 .zip 格式的 Skill 包,解压并校验是否存在 manifest.json 和 skill.md,然后将元信息写入数据库。
from flask import Flask, request, jsonify
import zipfile, os, json, shutil
app = Flask(__name__)
@app.route('/api/skills/upload', methods=['POST'])
def upload_skill():
file = request.files['package']
if not file or not file.filename.endswith('.zip'):
return jsonify({"error": "Only .zip packages are accepted"}), 400
# 解压到临时目录
tmp_dir = f'uploads/{os.path.splitext(file.filename)[0]}'
with zipfile.ZipFile(file, 'r') as z:
z.extractall(tmp_dir)
# 校验 manifest
manifest_path = os.path.join(tmp_dir, 'manifest.json')
if not os.path.exists(manifest_path):
shutil.rmtree(tmp_dir)
return jsonify({"error": "manifest.json missing"}), 400
with open(manifest_path) as f:
manifest = json.load(f)
if 'name' not in manifest or 'version' not in manifest:
shutil.rmtree(tmp_dir)
return jsonify({"error": "manifest must contain name and version"}), 400
# 校验 skill.md
md_path = os.path.join(tmp_dir, manifest.get('entrypoint', 'skill.md'))
if not os.path.exists(md_path):
shutil.rmtree(tmp_dir)
return jsonify({"error": "skill.md missing"}), 400
# 存储到 packages 目录
target = f"packages/{manifest['name']}/{manifest['version']}"
if os.path.exists(target):
shutil.rmtree(target)
shutil.move(tmp_dir, target)
# 写入数据库
conn = sqlite3.connect('registry.db')
c = conn.cursor()
c.execute('''INSERT OR REPLACE INTO skills
(name, version, description, author, min_agent_version, path)
VALUES (?, ?, ?, ?, ?, ?)''',
(manifest['name'], manifest['version'], manifest.get('description', ''),
manifest.get('author', ''), manifest.get('min_agent_version', ''), target))
conn.commit()
conn.close()
return jsonify({"status": "ok", "name": manifest['name'], "version": manifest['version']})
运行 python registry.py 后,即可通过 curl -F "package=@my-skill.zip" http://localhost:5000/api/skills/upload 上传 Skill。
预期结果:上传成功后,你会在 packages/code-review/1.1.0/ 下看到解压后的两个文件,并且数据库里多了一条记录。
2.4 搜索与版本列表 API
提供两个公开端点:按名称模糊搜索,以及查询某个 Skill 的所有历史版本。
@app.route('/api/skills/search')
def search():
q = request.args.get('q', '').lower()
conn = sqlite3.connect('registry.db')
c = conn.cursor()
c.execute("SELECT DISTINCT name, description, author FROM skills WHERE name LIKE ?", (f'%{q}%',))
results = [{'name': r[0], 'description': r[1], 'author': r[2]} for r in c.fetchall()]
conn.close()
return jsonify(results)
@app.route('/api/skills/<name>/versions')
def list_versions(name):
conn = sqlite3.connect('registry.db')
c = conn.cursor()
c.execute("SELECT version, created, min_agent_version FROM skills WHERE name=? ORDER BY created DESC", (name,))
versions = [{'version': r[0], 'created': r[1], 'min_agent_version': r[2]} for r in c.fetchall()]
conn.close()
return jsonify(versions)
预期结果:用 curl http://localhost:5000/api/skills/search?q=code 应该能返回包含 code-review 的列表;查看版本列表时,按上传时间倒序显示所有历史版本。
步骤三:搜索、安装与更新客户端
有了注册中心 API,我们还需要一个让用户无需关心底层 HTTP 调用的 CLI 工具。我们使用 Click 构建一个类似 npm 或 pip 的命令行客户端。
3.1 安装 Skill
客户端会先向注册中心查询目标 Skill 的版本列表,然后下载指定版本的包(可以是 .zip 或直接复用注册中心的服务端路径)。为简化,我们让注册中心额外提供一个直接下载文件夹内容的端点。
在 registry.py 中添加:
import io
@app.route('/api/skills/<name>/<version>/download')
def download_skill(name, version):
target = f'packages/{name}/{version}'
if not os.path.exists(target):
return jsonify({"error": "Version not found"}), 404
# 将目录打包为 zip 流返回
data = io.BytesIO()
with zipfile.ZipFile(data, 'w') as z:
for root, dirs, files in os.walk(target):
for f in files:
z.write(os.path.join(root, f), os.path.relpath(os.path.join(root, f), target))
data.seek(0)
return send_file(data, mimetype='application/zip', as_attachment=True, download_name=f'{name}-{version}.zip')
然后在客户端 cli.py 中实现安装逻辑:
import click, requests, zipfile, os, json, shutil
REGISTRY_URL = 'http://localhost:5000'
SKILLS_DIR = os.path.expanduser('~/.myagent/skills') # 默认安装位置
@click.group()
def cli():
pass
@cli.command()
@click.argument('name')
@click.option('--version', default=None, help='Target version, e.g., 1.1.0')
def install(name, version):
"""Install a skill from the registry"""
# 如果没有指定版本,找最新版
if version is None:
resp = requests.get(f'{REGISTRY_URL}/api/skills/{name}/versions')
if resp.status_code != 200 or not resp.json():
click.echo(f"Skill '{name}' not found.")
return
version = resp.json()[0]['version'] # 最新的排第一
# 下载
dl = requests.get(f'{REGISTRY_URL}/api/skills/{name}/{version}/download', stream=True)
if dl.status_code != 200:
click.echo(f"Version {version} not available.")
return
target_dir = os.path.join(SKILLS_DIR, name, version)
os.makedirs(target_dir, exist_ok=True)
with zipfile.ZipFile(dl.raw) as z:
z.extractall(target_dir)
click.echo(f"Skill {name}=={version} installed to {target_dir}")
# 读取依赖,依次安装(依赖解析将在下一步增强)
manifest_path = os.path.join(target_dir, 'manifest.json')
if os.path.exists(manifest_path):
with open(manifest_path) as f:
manifest = json.load(f)
for dep_name, dep_ver_range in manifest.get('dependencies', {}).items():
# 简单递归安装,实际应按版本范围选择
install_dependency(dep_name, dep_ver_range)
def install_dependency(name, version_range):
# 省略详细实现,可复用 install 逻辑并解析 range
pass
预期结果:在终端执行 python cli.py install code-review --version 1.1.0,你会在 ~/.myagent/skills/code-review/1.1.0/ 下看到完整的 Skill 包,并且它的依赖(如 code-style-check)也会被自动拉取。
3.2 更新 Skill
更新操作本质上是安装一个更高版本。我们提供一个 update 命令,它会对比本地已安装版本与注册中心最新版本,如果注册中心有更新的版本则自动安装。
@cli.command()
@click.argument('name')
def update(name):
# 获取已安装版本(简化为检查最高版本目录)
local_versions = []
local_base = os.path.join(SKILLS_DIR, name)
if os.path.exists(local_base):
local_versions = os.listdir(local_base)
# 获取注册中心最新版本
resp = requests.get(f'{REGISTRY_URL}/api/skills/{name}/versions')
if resp.status_code != 200:
click.echo(f"Skill '{name}' not in registry.")
return
remote_latest = resp.json()[0]['version']
if not local_versions or remote_latest not in local_versions:
click.echo(f"Updating {name} to {remote_latest}...")
install.callback(name, version=remote_latest)
else:
click.echo(f"{name} is already up-to-date.")
踩坑经验:注册中心的下载端点如果直接提供目录路径,需要特别注意路径遍历攻击。你永远不应该信任用户输入的
name和version参数,务必进行严格的字符白名单过滤(只允许字母、数字、连字符和点号),并检查实际路径是否确实落在packages/目录下。本章示例为简化略去了这些检查,但在生产环境中必须加上。
步骤四:依赖解析与版本冲突处理
如果只是粗暴地“先下载最新版”,当两个 Skill 依赖同一个库的不同版本时(比如 A 要求 code-style-check ~0.9.0,B 要求 code-style-check ^1.0.0),Agent 就会陷入混乱。语义版本(SemVer)和海象算符(~、^)正是为解决这类问题而生的。
4.1 版本范围约定
我们在 manifest 的 dependencies 值中使用标准的 npm 式范围表示法:
^1.2.3表示兼容 1.x 的最新版本(Major 相同,Minor 和 Patch 可以更高)~1.2.3表示 1.2.x 的任意 Patch>=1.0.0或1.0.0 - 2.0.0等也可接受,但推荐使用^和~
示例:
"dependencies": {
"code-style-check": "~0.9.0"
}
这表示允许 0.9.0 到 0.9.x 的任意修订版本,但不能升级到 0.10.0(可能会引入不兼容的变更)。
4.2 在安装流程中集成解析器
我们可以使用 Python 的 semver 库(需另外安装)来处理版本比较,但为了演示核心逻辑,这里手动实现一个简易的比较器。
在客户端 CLI 中,增加 resolve_dependencies 函数,它会:
- 收集所有已安装的直接依赖和间接依赖
- 对于每个新加入的依赖,检查其版本范围是否与已有版本冲突
- 若无冲突,选择满足所有约束的最高版本;若冲突,报错并提示冲突来源
from collections import defaultdict
def resolve_dependencies(name, version_range, resolved=None, conflict_check=None):
if resolved is None:
resolved = {}
if conflict_check is None:
conflict_check = defaultdict(list)
# 查找注册中心中符合 version_range 的所有版本
resp = requests.get(f'{REGISTRY_URL}/api/skills/{name}/versions')
if resp.status_code != 200:
raise Exception(f"Skill {name} not found.")
candidates = [v['version'] for v in resp.json()]
best = pick_best_version(candidates, version_range)
if not best:
raise Exception(f"No version of {name} matches range {version_range}")
# 检查冲突:如果 name 已经被解析过,且与之前的范围冲突
if name in resolved:
# 已解析的版本必须同时满足所有记录的范围
if not version_in_range(resolved[name], version_range):
conflict_check[name].append(version_range)
raise Exception(f"Conflict: {name} required by multiple with incompatible ranges: {conflict_check[name]}")
else:
resolved[name] = best
# 读取该版本的 manifest.json 以获取其依赖
target_dir = os.path.join(SKILLS_DIR, name, best)
# 如果在本地还没安装,则先安装(这里仅演示递归解析)
# 实际流程中,解析完所有依赖树后再统一安装
# ...
上面的伪代码展示了核心思路。生产级实现建议使用现成的依赖解析库(如 Python 的 pip 内部的解析器模型),或者直接采用 Go mod / Cargo 的解析策略——优先满足直接依赖,若出现 Diamond Dependency 冲突则提示用户手动选择。
预期结果:当你尝试安装一个要求 code-style-check ~0.9.0 的 Skill,而另一个已安装的 Skill 要求 code-style-check =0.8.5 时,命令行会立即报错并列出冲突:
Error: Dependency conflict for code-style-check:
- code-review requires ~0.9.0
- code-review-legacy requires =0.8.5
Please resolve manually.
4.3 关于依赖锁文件
强烈建议在团队项目中使用一个 锁文件(如 skills.lock),记录每次安装后确切的依赖树和版本哈希。这样可以保证所有开发人员环境一致,也便于 CI/CD 重现构建。锁文件结构可以仿照 package-lock.json,记录每个 Skill 的名称、版本和校验和。
回顾
本章我们完成了:
- 定义了一套标准化的 Skills 包格式,包含
manifest.json和skill.md - 使用 Flask + SQLite 搭建了一个带上传、搜索、版本列表、下载功能的注册中心后端
- 用 Click 编写了一个 CLI 客户端,实现了
install和update命令 - 引入了语义版本与依赖范围解析,在安装时自动处理依赖树并检测版本冲突
整个过程从零开始构建了一个内部 Skills 市场的最小可用原型。实际落地时,你还可以进一步添加 Web 管理界面、身份认证、审批流程等功能。
下一步行动清单:
- 在团队内部试用上传和安装流程,确保
manifest.json填写无误 - 为高优先级的 Skill 补全
dependencies声明,并测试冲突场景 - 将注册中心后端部署到内网服务器,并配置持久化存储
- 给 CLI 增加
skill list命令,展示本地已安装的 Skill 及其版本 - 开始规划接入认证机制,为下一章做准备
下一章《认证、授权与审计缺一不可》将为你构建的注册中心加上多层级权限控制,确保只有授权用户才能发布、安装或更新 Skill,并且每一次操作都留有审计记录——这是 Skills 市场从“能用”走向“安全可控”的关键一步。
agent skills 入门到精通
关于 LearnKu