mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
Merge pull request #72 from iflytek/fix/global-public-download-anon
Allow anonymous downloads for global public skills
This commit is contained in:
commit
8708cab18c
8 changed files with 751 additions and 30 deletions
22
README.md
22
README.md
|
|
@ -302,22 +302,26 @@ Run it against a local backend:
|
|||
|
||||
SkillHub works as a skill registry backend for several agent platforms. Point any of the clients below at your SkillHub instance to publish, discover, and install skills.
|
||||
|
||||
### [openclaw](https://github.com/openclaw/openclaw)
|
||||
### [OpenClaw](https://github.com/openclaw/openclaw)
|
||||
|
||||
[openclaw](https://github.com/openclaw/openclaw) is an open-source agent skill CLI. Configure it to use your SkillHub endpoint as the registry:
|
||||
[OpenClaw](https://github.com/openclaw/openclaw) is an open-source agent skill CLI. Configure it to use your SkillHub endpoint as the registry:
|
||||
|
||||
```bash
|
||||
# Log in to your SkillHub instance
|
||||
openclaw login --registry https://<your-skillhub-host>
|
||||
|
||||
# Publish a skill
|
||||
openclaw push <skill-package>
|
||||
# Configure registry URL
|
||||
export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com
|
||||
export CLAWHUB_API_TOKEN=YOUR_API_TOKEN
|
||||
|
||||
# Search and install skills
|
||||
openclaw search <keyword>
|
||||
openclaw install <namespace>/<skill>
|
||||
npx clawhub search email
|
||||
npx clawhub install my-skill
|
||||
npx clawhub install my-namespace--my-skill
|
||||
|
||||
# Publish a skill
|
||||
npx clawhub publish ./my-skill
|
||||
```
|
||||
|
||||
📖 **[Complete OpenClaw Integration Guide →](./docs/openclaw-integration.md)**
|
||||
|
||||
### [AstronClaw](https://agent.xfyun.cn/astron-claw)
|
||||
|
||||
[AstronClaw](https://agent.xfyun.cn/astron-claw) is the skill marketplace provided by iFlytek's Astron platform. You can connect it to a self-hosted SkillHub registry to manage and distribute private skills within your organization, or browse publicly shared skills on the Astron platform.
|
||||
|
|
|
|||
20
README_zh.md
20
README_zh.md
|
|
@ -255,6 +255,26 @@ SkillHub 采用清晰的分层架构:
|
|||
|
||||
SkillHub 设计为与各种智能体平台和框架无缝集成。
|
||||
|
||||
### [OpenClaw](https://github.com/openclaw/openclaw)
|
||||
|
||||
[OpenClaw](https://github.com/openclaw/openclaw) 是开源的智能体技能 CLI 工具。配置它使用您的 SkillHub 端点作为注册中心:
|
||||
|
||||
```bash
|
||||
# 配置注册中心地址
|
||||
export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com
|
||||
export CLAWHUB_API_TOKEN=YOUR_API_TOKEN
|
||||
|
||||
# 搜索和安装技能
|
||||
npx clawhub search email
|
||||
npx clawhub install my-skill
|
||||
npx clawhub install my-namespace--my-skill
|
||||
|
||||
# 发布技能
|
||||
npx clawhub publish ./my-skill
|
||||
```
|
||||
|
||||
📖 **[完整 OpenClaw 集成指南 →](./docs/openclaw-integration.md)**
|
||||
|
||||
### [AstronClaw](https://agent.xfyun.cn/astron-claw)
|
||||
|
||||
[AstronClaw](https://agent.xfyun.cn/astron-claw) 是科大讯飞星火平台提供的技能市场。您可以将其连接到自托管的 SkillHub 注册中心,在组织内管理和分发私有技能,或在星火平台上浏览公开共享的技能。
|
||||
|
|
|
|||
307
docs/openclaw-integration-en.md
Normal file
307
docs/openclaw-integration-en.md
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
# OpenClaw Integration Guide
|
||||
|
||||
This document explains how to configure OpenClaw CLI to connect to a SkillHub private registry for publishing, searching, and downloading skills.
|
||||
|
||||
## Overview
|
||||
|
||||
SkillHub provides a ClawHub-compatible API layer, allowing OpenClaw CLI to seamlessly integrate with private registries. With simple configuration, you can:
|
||||
|
||||
- 🔍 Search for private skills within your organization
|
||||
- 📥 Download and install skill packages
|
||||
- 📤 Publish new skills to the private registry
|
||||
- ⭐ Star and rate skills
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Configure Registry URL
|
||||
|
||||
Set the SkillHub registry address in your OpenClaw configuration:
|
||||
|
||||
```bash
|
||||
# Via environment variable
|
||||
export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com
|
||||
```
|
||||
|
||||
### 2. Authentication (Optional)
|
||||
|
||||
For **global namespace (@global) PUBLIC skills**, no login is required to download. Authentication is required for:
|
||||
|
||||
- Team namespace skills (regardless of visibility)
|
||||
- NAMESPACE_ONLY or PRIVATE skills
|
||||
- Write operations like publishing, starring, etc.
|
||||
|
||||
```bash
|
||||
# Using API Token
|
||||
export CLAWHUB_API_TOKEN=YOUR_API_TOKEN
|
||||
```
|
||||
|
||||
#### Obtaining an API Token
|
||||
|
||||
1. Log in to SkillHub Web UI
|
||||
2. Navigate to **Settings → API Tokens**
|
||||
3. Click **Create New Token**
|
||||
4. Set token name and permissions
|
||||
5. Copy the generated token
|
||||
|
||||
### 3. Search Skills
|
||||
|
||||
```bash
|
||||
# Search for skills
|
||||
npx clawhub search email
|
||||
|
||||
# View skill details
|
||||
npx clawhub info my-skill
|
||||
```
|
||||
|
||||
### 4. Install Skills
|
||||
|
||||
```bash
|
||||
# Install latest version
|
||||
npx clawhub install my-skill
|
||||
|
||||
# Install specific version
|
||||
npx clawhub install my-skill@1.2.0
|
||||
|
||||
# Install skill from team namespace
|
||||
npx clawhub install my-namespace--my-skill
|
||||
```
|
||||
|
||||
### 5. Publish Skills
|
||||
|
||||
```bash
|
||||
# Publish a skill (requires appropriate permissions)
|
||||
npx clawhub publish ./my-skill
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
SkillHub compatibility layer provides the following endpoints:
|
||||
|
||||
| Endpoint | Method | Description | Auth Required |
|
||||
|----------|--------|-------------|---------------|
|
||||
| `/api/v1/whoami` | GET | Get current user info | Yes |
|
||||
| `/api/v1/search` | GET | Search skills | Optional |
|
||||
| `/api/v1/resolve` | GET | Resolve skill version | Optional |
|
||||
| `/api/v1/download/{slug}` | GET | Download skill (redirect) | Optional* |
|
||||
| `/api/v1/download` | GET | Download skill (query params) | Optional* |
|
||||
| `/api/v1/skills/{slug}` | GET | Get skill details | Optional |
|
||||
| `/api/v1/skills/{slug}/star` | POST | Star a skill | Yes |
|
||||
| `/api/v1/skills/{slug}/unstar` | DELETE | Unstar a skill | Yes |
|
||||
| `/api/v1/publish` | POST | Publish a skill | Yes |
|
||||
|
||||
\* Download endpoint authentication requirements:
|
||||
- **Global namespace (@global) PUBLIC skills**: No authentication required
|
||||
- **All team namespace skills**: Authentication required
|
||||
- **NAMESPACE_ONLY and PRIVATE skills**: Authentication required
|
||||
|
||||
## Skill Visibility Levels
|
||||
|
||||
SkillHub supports three visibility levels with the following download permission rules:
|
||||
|
||||
### PUBLIC
|
||||
- ✅ Anyone can search and view
|
||||
- ✅ **Global namespace (@global)**: No login required to download
|
||||
- 🔒 **Team namespaces**: Authentication required to download
|
||||
- 📍 Suitable for organization-wide, publicly shareable skills
|
||||
|
||||
### NAMESPACE_ONLY
|
||||
- ✅ Namespace members can search and view
|
||||
- 🔒 Login required and must be namespace member to download
|
||||
- 📍 Suitable for team-internal skills
|
||||
|
||||
### PRIVATE
|
||||
- ✅ Only owner can view
|
||||
- 🔒 Login required and must be owner to download
|
||||
- 📍 Suitable for skills under personal development
|
||||
|
||||
**Important Notes**:
|
||||
- Global namespace (`@global`) PUBLIC skills support anonymous downloads for wide distribution within the organization
|
||||
- All team namespace skills (including PUBLIC) require authentication to ensure team boundary security
|
||||
|
||||
## Canonical Slug Mapping
|
||||
|
||||
SkillHub internally uses `@{namespace}/{skill}` format, but the compatibility layer automatically converts to ClawHub-style canonical slugs:
|
||||
|
||||
| SkillHub Internal | Canonical Slug | Description |
|
||||
|-------------------|----------------|-------------|
|
||||
| `@global/my-skill` | `my-skill` | Global namespace skill |
|
||||
| `@my-team/my-skill` | `my-team--my-skill` | Team namespace skill |
|
||||
|
||||
OpenClaw CLI uses canonical slug format, and SkillHub handles the conversion automatically.
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### ClawHub CLI Environment Variables
|
||||
|
||||
ClawHub CLI is configured via environment variables:
|
||||
|
||||
```bash
|
||||
# Registry configuration
|
||||
export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com
|
||||
export CLAWHUB_API_TOKEN=sk_your_api_token_here
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Registry configuration
|
||||
export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com
|
||||
export CLAWHUB_API_TOKEN=sk_your_api_token_here
|
||||
|
||||
# Optional: Skip SSL verification (development only)
|
||||
export CLAWHUB_SKIP_SSL_VERIFY=false
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: How do I switch back to public ClawHub?
|
||||
|
||||
```bash
|
||||
# Unset custom registry
|
||||
unset CLAWHUB_REGISTRY_URL
|
||||
|
||||
# ClawHub CLI will use the default public registry
|
||||
```
|
||||
|
||||
### Q: Getting 403 Forbidden when downloading?
|
||||
|
||||
Possible causes:
|
||||
1. Skill belongs to a team namespace, authentication required
|
||||
2. Skill is NAMESPACE_ONLY or PRIVATE, authentication required
|
||||
3. You're not a member of the namespace
|
||||
4. API Token has expired
|
||||
|
||||
Solution:
|
||||
```bash
|
||||
# Set new token
|
||||
export CLAWHUB_API_TOKEN=YOUR_NEW_TOKEN
|
||||
|
||||
# Test connection
|
||||
curl https://skillhub.your-company.com/api/v1/whoami \
|
||||
-H "Authorization: Bearer $CLAWHUB_API_TOKEN"
|
||||
```
|
||||
|
||||
**Tip**: Global namespace (@global) PUBLIC skills can be downloaded anonymously without authentication.
|
||||
|
||||
### Q: How do I see all skills I have access to?
|
||||
|
||||
```bash
|
||||
# Search all skills (filtered by permissions)
|
||||
npx clawhub search ""
|
||||
```
|
||||
|
||||
### Q: Permission denied when publishing?
|
||||
|
||||
- Publishing to global namespace (`@global`) requires `SUPER_ADMIN` permission
|
||||
- Publishing to team namespace requires OWNER or ADMIN role in that namespace
|
||||
- Contact your administrator for appropriate permissions
|
||||
|
||||
### Q: Which OpenClaw versions are supported?
|
||||
|
||||
SkillHub compatibility layer is designed to work with tools using ClawHub CLI. ClawHub CLI is distributed via npm:
|
||||
|
||||
```bash
|
||||
# Install ClawHub CLI
|
||||
npm install -g clawhub
|
||||
|
||||
# Or use npx directly
|
||||
npx clawhub install my-skill
|
||||
```
|
||||
|
||||
If you encounter compatibility issues, please file an issue.
|
||||
|
||||
## API Response Formats
|
||||
|
||||
### Search Response Example
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"slug": "my-team--email-sender",
|
||||
"name": "Email Sender",
|
||||
"description": "Send emails via SMTP",
|
||||
"author": {
|
||||
"handle": "user123",
|
||||
"displayName": "John Doe"
|
||||
},
|
||||
"version": "1.2.0",
|
||||
"downloadCount": 150,
|
||||
"starCount": 25,
|
||||
"createdAt": "2026-01-15T10:00:00Z",
|
||||
"updatedAt": "2026-03-10T14:30:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"limit": 20
|
||||
}
|
||||
```
|
||||
|
||||
### Version Resolution Response Example
|
||||
|
||||
```json
|
||||
{
|
||||
"slug": "my-skill",
|
||||
"version": "1.2.0",
|
||||
"downloadUrl": "/api/v1/skills/global/my-skill/versions/1.2.0/download"
|
||||
}
|
||||
```
|
||||
|
||||
### Publish Response Example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "12345",
|
||||
"version": {
|
||||
"id": "67890"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
1. **Use HTTPS**: Always use HTTPS in production
|
||||
2. **Token Management**:
|
||||
- Rotate API tokens regularly
|
||||
- Never hardcode tokens in code
|
||||
- Use environment variables or secret management tools
|
||||
3. **Least Privilege**: Assign minimum required permissions to tokens
|
||||
4. **Audit Logs**: Regularly review SkillHub audit logs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
```bash
|
||||
# View detailed request logs
|
||||
DEBUG=clawhub:* npx clawhub search my-skill
|
||||
|
||||
# Or use verbose mode
|
||||
npx clawhub --verbose install my-skill
|
||||
```
|
||||
|
||||
### Test Connection
|
||||
|
||||
```bash
|
||||
# Test registry connection
|
||||
curl https://skillhub.your-company.com/api/v1/whoami \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# Test search
|
||||
curl "https://skillhub.your-company.com/api/v1/search?q=test"
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [SkillHub API Design](./06-api-design.md)
|
||||
- [Skill Protocol Specification](./07-skill-protocol.md)
|
||||
- [Authentication & Authorization](./03-authentication-design.md)
|
||||
- [Deployment Guide](./09-deployment.md)
|
||||
|
||||
## Support
|
||||
|
||||
For questions or suggestions:
|
||||
- 📖 Full Documentation: https://zread.ai/iflytek/skillhub
|
||||
- 💬 GitHub Discussions: https://github.com/iflytek/skillhub/discussions
|
||||
- 🐛 Submit Issues: https://github.com/iflytek/skillhub/issues
|
||||
307
docs/openclaw-integration.md
Normal file
307
docs/openclaw-integration.md
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
# OpenClaw 集成指南
|
||||
|
||||
本文档说明如何配置 OpenClaw CLI 连接到 SkillHub 私有注册中心,实现技能的发布、搜索和下载。
|
||||
|
||||
## 概述
|
||||
|
||||
SkillHub 提供了与 ClawHub 兼容的 API 层,使得 OpenClaw CLI 可以无缝对接私有注册中心。通过简单的配置,您可以:
|
||||
|
||||
- 🔍 搜索组织内的私有技能
|
||||
- 📥 下载和安装技能包
|
||||
- 📤 发布新技能到私有注册中心
|
||||
- ⭐ 收藏和评分技能
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 配置 Registry 地址
|
||||
|
||||
在 OpenClaw 配置文件中设置 SkillHub 注册中心地址:
|
||||
|
||||
```bash
|
||||
# 通过环境变量配置
|
||||
export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com
|
||||
```
|
||||
|
||||
### 2. 登录认证(可选)
|
||||
|
||||
对于**全局命名空间(@global)的公开技能(PUBLIC)**,无需登录即可下载。对于以下情况需要认证:
|
||||
|
||||
- 团队命名空间的技能(无论可见性)
|
||||
- NAMESPACE_ONLY 或 PRIVATE 技能
|
||||
- 发布、收藏等写操作
|
||||
|
||||
```bash
|
||||
# 使用 API Token
|
||||
export CLAWHUB_API_TOKEN=YOUR_API_TOKEN
|
||||
```
|
||||
|
||||
#### 获取 API Token
|
||||
|
||||
1. 登录 SkillHub Web UI
|
||||
2. 进入 **个人设置 → API Tokens**
|
||||
3. 点击 **创建新 Token**
|
||||
4. 设置 Token 名称和权限范围
|
||||
5. 复制生成的 Token
|
||||
|
||||
### 3. 搜索技能
|
||||
|
||||
```bash
|
||||
# 搜索技能
|
||||
npx clawhub search email
|
||||
|
||||
# 查看技能详情
|
||||
npx clawhub info my-skill
|
||||
```
|
||||
|
||||
### 4. 安装技能
|
||||
|
||||
```bash
|
||||
# 安装最新版本
|
||||
npx clawhub install my-skill
|
||||
|
||||
# 安装指定版本
|
||||
npx clawhub install my-skill@1.2.0
|
||||
|
||||
# 安装团队命名空间下的技能
|
||||
npx clawhub install my-namespace--my-skill
|
||||
```
|
||||
|
||||
### 5. 发布技能
|
||||
|
||||
```bash
|
||||
# 发布技能(需要相应权限)
|
||||
npx clawhub publish ./my-skill
|
||||
```
|
||||
|
||||
## API 端点说明
|
||||
|
||||
SkillHub 兼容层提供以下端点:
|
||||
|
||||
| 端点 | 方法 | 说明 | 认证要求 |
|
||||
|------|------|------|----------|
|
||||
| `/api/v1/whoami` | GET | 获取当前用户信息 | 必需 |
|
||||
| `/api/v1/search` | GET | 搜索技能 | 可选 |
|
||||
| `/api/v1/resolve` | GET | 解析技能版本 | 可选 |
|
||||
| `/api/v1/download/{slug}` | GET | 下载技能(重定向) | 可选* |
|
||||
| `/api/v1/download` | GET | 下载技能(查询参数) | 可选* |
|
||||
| `/api/v1/skills/{slug}` | GET | 获取技能详情 | 可选 |
|
||||
| `/api/v1/skills/{slug}/star` | POST | 收藏技能 | 必需 |
|
||||
| `/api/v1/skills/{slug}/unstar` | DELETE | 取消收藏 | 必需 |
|
||||
| `/api/v1/publish` | POST | 发布技能 | 必需 |
|
||||
|
||||
\* 下载端点认证要求:
|
||||
- **全局命名空间(@global)的 PUBLIC 技能**:无需认证
|
||||
- **团队命名空间的所有技能**:需要认证
|
||||
- **NAMESPACE_ONLY 和 PRIVATE 技能**:需要认证
|
||||
|
||||
## 技能可见性说明
|
||||
|
||||
SkillHub 支持三种技能可见性级别,下载权限规则如下:
|
||||
|
||||
### PUBLIC(公开)
|
||||
- ✅ 任何人都可以搜索和查看
|
||||
- ✅ **全局命名空间(@global)**:无需登录即可下载
|
||||
- 🔒 **团队命名空间**:需要登录认证才能下载
|
||||
- 📍 适用于组织内通用的、可公开分享的技能
|
||||
|
||||
### NAMESPACE_ONLY(命名空间内可见)
|
||||
- ✅ 命名空间成员可以搜索和查看
|
||||
- 🔒 需要登录且是命名空间成员才能下载
|
||||
- 📍 适用于团队内部技能
|
||||
|
||||
### PRIVATE(私有)
|
||||
- ✅ 仅所有者可以查看
|
||||
- 🔒 需要登录且是所有者才能下载
|
||||
- 📍 适用于个人开发中的技能
|
||||
|
||||
**重要说明**:
|
||||
- 全局命名空间(`@global`)的 PUBLIC 技能支持匿名下载,便于组织内广泛分发
|
||||
- 团队命名空间的所有技能(包括 PUBLIC)都需要认证,确保团队边界安全
|
||||
|
||||
## Canonical Slug 映射规则
|
||||
|
||||
SkillHub 内部使用 `@{namespace}/{skill}` 格式,但兼容层会自动转换为 ClawHub 风格的 canonical slug:
|
||||
|
||||
| SkillHub 内部坐标 | Canonical Slug | 说明 |
|
||||
|-------------------|----------------|------|
|
||||
| `@global/my-skill` | `my-skill` | 全局命名空间技能 |
|
||||
| `@my-team/my-skill` | `my-team--my-skill` | 团队命名空间技能 |
|
||||
|
||||
OpenClaw CLI 使用 canonical slug 格式,SkillHub 会自动处理转换。
|
||||
|
||||
## 配置示例
|
||||
|
||||
### ClawHub CLI 环境变量配置
|
||||
|
||||
ClawHub CLI 通过环境变量配置:
|
||||
|
||||
```bash
|
||||
# Registry 配置
|
||||
export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com
|
||||
export CLAWHUB_API_TOKEN=sk_your_api_token_here
|
||||
```
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
```bash
|
||||
# Registry 配置
|
||||
export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com
|
||||
export CLAWHUB_API_TOKEN=sk_your_api_token_here
|
||||
|
||||
# 可选:跳过 SSL 验证(仅用于开发环境)
|
||||
export CLAWHUB_SKIP_SSL_VERIFY=false
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何切换回公共 ClawHub?
|
||||
|
||||
```bash
|
||||
# 取消设置自定义 Registry
|
||||
unset CLAWHUB_REGISTRY_URL
|
||||
|
||||
# ClawHub CLI 将使用默认的公共注册中心
|
||||
```
|
||||
|
||||
### Q: 下载技能时提示 403 Forbidden?
|
||||
|
||||
可能原因:
|
||||
1. 技能属于团队命名空间,需要登录
|
||||
2. 技能是 NAMESPACE_ONLY 或 PRIVATE,需要登录
|
||||
3. 您不是该命名空间的成员
|
||||
4. API Token 已过期
|
||||
|
||||
解决方法:
|
||||
```bash
|
||||
# 设置新的 Token
|
||||
export CLAWHUB_API_TOKEN=YOUR_NEW_TOKEN
|
||||
|
||||
# 测试连接
|
||||
curl https://skillhub.your-company.com/api/v1/whoami \
|
||||
-H "Authorization: Bearer $CLAWHUB_API_TOKEN"
|
||||
```
|
||||
|
||||
**提示**:全局命名空间(@global)的 PUBLIC 技能可以匿名下载,无需认证。
|
||||
|
||||
### Q: 如何查看我有权访问的所有技能?
|
||||
|
||||
```bash
|
||||
# 搜索所有技能(会根据权限过滤)
|
||||
npx clawhub search ""
|
||||
```
|
||||
|
||||
### Q: 发布技能时提示权限不足?
|
||||
|
||||
- 发布到全局命名空间(`@global`)需要 `SUPER_ADMIN` 权限
|
||||
- 发布到团队命名空间需要是该命名空间的 OWNER 或 ADMIN
|
||||
- 联系管理员分配相应权限
|
||||
|
||||
### Q: 支持哪些 OpenClaw 版本?
|
||||
|
||||
SkillHub 兼容层设计兼容使用 ClawHub CLI 的工具。ClawHub CLI 通过 npm 分发:
|
||||
|
||||
```bash
|
||||
# 安装 ClawHub CLI
|
||||
npm install -g clawhub
|
||||
|
||||
# 或使用 npx 直接运行
|
||||
npx clawhub install my-skill
|
||||
```
|
||||
|
||||
如遇到兼容性问题,请提交 Issue。
|
||||
|
||||
## API 响应格式
|
||||
|
||||
### 搜索响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"slug": "my-team--email-sender",
|
||||
"name": "Email Sender",
|
||||
"description": "Send emails via SMTP",
|
||||
"author": {
|
||||
"handle": "user123",
|
||||
"displayName": "John Doe"
|
||||
},
|
||||
"version": "1.2.0",
|
||||
"downloadCount": 150,
|
||||
"starCount": 25,
|
||||
"createdAt": "2026-01-15T10:00:00Z",
|
||||
"updatedAt": "2026-03-10T14:30:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"limit": 20
|
||||
}
|
||||
```
|
||||
|
||||
### 版本解析响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"slug": "my-skill",
|
||||
"version": "1.2.0",
|
||||
"downloadUrl": "/api/v1/skills/global/my-skill/versions/1.2.0/download"
|
||||
}
|
||||
```
|
||||
|
||||
### 发布响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "12345",
|
||||
"version": {
|
||||
"id": "67890"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **使用 HTTPS**:生产环境务必使用 HTTPS 连接
|
||||
2. **Token 管理**:
|
||||
- 定期轮换 API Token
|
||||
- 不要在代码中硬编码 Token
|
||||
- 使用环境变量或密钥管理工具
|
||||
3. **权限最小化**:为 Token 分配最小必需权限
|
||||
4. **审计日志**:定期检查 SkillHub 审计日志
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 启用调试日志
|
||||
|
||||
```bash
|
||||
# 查看详细请求日志
|
||||
DEBUG=clawhub:* npx clawhub search my-skill
|
||||
|
||||
# 或使用 verbose 模式
|
||||
npx clawhub --verbose install my-skill
|
||||
```
|
||||
|
||||
### 测试连接
|
||||
|
||||
```bash
|
||||
# 测试 Registry 连接
|
||||
curl https://skillhub.your-company.com/api/v1/whoami \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# 测试搜索
|
||||
curl "https://skillhub.your-company.com/api/v1/search?q=test"
|
||||
```
|
||||
|
||||
## 进一步阅读
|
||||
|
||||
- [SkillHub API 设计文档](./06-api-design.md)
|
||||
- [技能协议规范](./07-skill-protocol.md)
|
||||
- [认证与授权](./03-authentication-design.md)
|
||||
- [部署指南](./09-deployment.md)
|
||||
|
||||
## 支持
|
||||
|
||||
如有问题或建议:
|
||||
- 📖 查看完整文档:https://zread.ai/iflytek/skillhub
|
||||
- 💬 GitHub Discussions:https://github.com/iflytek/skillhub/discussions
|
||||
- 🐛 提交 Issue:https://github.com/iflytek/skillhub/issues
|
||||
|
|
@ -10,6 +10,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||
import com.iflytek.skillhub.TestRedisConfig;
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillDownloadService;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillQueryService;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
|
@ -99,13 +100,29 @@ class SkillControllerDownloadTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void downloadVersion_requiresAuthentication() throws Exception {
|
||||
void downloadVersion_allowsAnonymousForGlobalSkill() throws Exception {
|
||||
given(skillDownloadService.downloadVersion("global", "demo-skill", "1.0.0", null, java.util.Map.of()))
|
||||
.willReturn(new SkillDownloadService.DownloadResult(
|
||||
new ByteArrayInputStream("zip".getBytes()),
|
||||
"demo-skill-1.0.0.zip",
|
||||
3L,
|
||||
"application/zip",
|
||||
null
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/v1/skills/global/demo-skill/versions/1.0.0/download")
|
||||
.with(csrf()))
|
||||
.andDo(result -> {
|
||||
System.out.println("Status: " + result.getResponse().getStatus());
|
||||
System.out.println("Body: " + result.getResponse().getContentAsString());
|
||||
})
|
||||
.andExpect(status().isUnauthorized());
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Disposition", "attachment; filename=\"demo-skill-1.0.0.zip\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadVersion_forbidsAnonymousWhenServiceRejectsSkill() throws Exception {
|
||||
given(skillDownloadService.downloadVersion("team-ai", "demo-skill", "1.0.0", null, java.util.Map.of()))
|
||||
.willThrow(new DomainForbiddenException("error.skill.access.denied", "demo-skill"));
|
||||
|
||||
mockMvc.perform(get("/api/v1/skills/team-ai/demo-skill/versions/1.0.0/download")
|
||||
.with(csrf()))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,20 +135,26 @@ public class SecurityConfig {
|
|||
"/api/v1/skills/*/*",
|
||||
"/api/v1/skills/*/*/versions",
|
||||
"/api/v1/skills/*/*/versions/*",
|
||||
"/api/v1/skills/*/*/download",
|
||||
"/api/v1/skills/*/*/versions/*/download",
|
||||
"/api/v1/skills/*/*/versions/*/files",
|
||||
"/api/v1/skills/*/*/versions/*/file",
|
||||
"/api/v1/skills/*/*/resolve",
|
||||
"/api/v1/skills/*/*/tags",
|
||||
"/api/v1/skills/*/*/tags/*/download",
|
||||
"/api/v1/skills/*/*/tags/*/files",
|
||||
"/api/v1/skills/*/*/tags/*/file",
|
||||
"/api/web/skills",
|
||||
"/api/web/skills/*/*",
|
||||
"/api/web/skills/*/*/versions",
|
||||
"/api/web/skills/*/*/versions/*",
|
||||
"/api/web/skills/*/*/download",
|
||||
"/api/web/skills/*/*/versions/*/download",
|
||||
"/api/web/skills/*/*/versions/*/files",
|
||||
"/api/web/skills/*/*/versions/*/file",
|
||||
"/api/web/skills/*/*/resolve",
|
||||
"/api/web/skills/*/*/tags",
|
||||
"/api/web/skills/*/*/tags/*/download",
|
||||
"/api/web/skills/*/*/tags/*/files",
|
||||
"/api/web/skills/*/*/tags/*/file"
|
||||
).permitAll()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.iflytek.skillhub.domain.event.SkillDownloadedEvent;
|
|||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
|
|
@ -72,11 +73,7 @@ public class SkillDownloadService {
|
|||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
assertCanDownload(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
if (skill.getLatestVersionId() == null) {
|
||||
throw new DomainBadRequestException("error.skill.version.latest.unavailable", skillSlug);
|
||||
|
|
@ -97,11 +94,7 @@ public class SkillDownloadService {
|
|||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
assertCanDownload(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillVersion version = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), versionStr)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", versionStr));
|
||||
|
|
@ -118,11 +111,7 @@ public class SkillDownloadService {
|
|||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
assertCanDownload(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillTag tag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tagName)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.tag.notFound", tagName));
|
||||
|
|
@ -217,6 +206,23 @@ public class SkillDownloadService {
|
|||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
private void assertCanDownload(Namespace namespace,
|
||||
Skill skill,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
if (currentUserId == null && !isAnonymousDownloadAllowed(namespace, skill)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skill.getSlug());
|
||||
}
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skill.getSlug());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAnonymousDownloadAllowed(Namespace namespace, Skill skill) {
|
||||
return namespace.getType() == NamespaceType.GLOBAL
|
||||
&& skill.getVisibility() == SkillVisibility.PUBLIC;
|
||||
}
|
||||
|
||||
private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) {
|
||||
return skillSlugResolutionService.resolve(
|
||||
namespaceId,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import com.iflytek.skillhub.domain.event.SkillDownloadedEvent;
|
|||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectMetadata;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
|
|
@ -267,6 +269,58 @@ class SkillDownloadServiceTest {
|
|||
verify(eventPublisher).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadVersion_AllowsAnonymousForGlobalPublicSkill() throws Exception {
|
||||
Namespace namespace = new Namespace("global", "Global", "system");
|
||||
setId(namespace, 1L);
|
||||
namespace.setType(NamespaceType.GLOBAL);
|
||||
|
||||
Skill skill = new Skill(1L, "demo-skill", "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setDisplayName("Demo Skill");
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", "owner-1");
|
||||
setId(version, 10L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
||||
when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, null, Map.of())).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, "1.0.0")).thenReturn(Optional.of(version));
|
||||
when(objectStorageService.exists("packages/1/10/bundle.zip")).thenReturn(false);
|
||||
when(skillFileRepository.findByVersionId(10L)).thenReturn(List.of(
|
||||
new SkillFile(10L, "SKILL.md", 4L, "text/markdown", "hash", "skills/1/10/SKILL.md")));
|
||||
when(objectStorageService.exists("skills/1/10/SKILL.md")).thenReturn(true);
|
||||
when(objectStorageService.getObject("skills/1/10/SKILL.md")).thenReturn(new ByteArrayInputStream("test".getBytes()));
|
||||
|
||||
SkillDownloadService.DownloadResult result = service.downloadVersion("global", "demo-skill", "1.0.0", null, Map.of());
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("Demo Skill-1.0.0.zip", result.filename());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadVersion_RejectsAnonymousForTeamNamespacePublicSkill() throws Exception {
|
||||
Namespace namespace = new Namespace("team-ai", "Team AI", "owner-1");
|
||||
setId(namespace, 2L);
|
||||
namespace.setType(NamespaceType.TEAM);
|
||||
|
||||
Skill skill = new Skill(2L, "demo-skill", "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
when(namespaceRepository.findBySlug("team-ai")).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(2L, "demo-skill")).thenReturn(List.of(skill));
|
||||
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
service.downloadVersion("team-ai", "demo-skill", "1.0.0", null, Map.of()));
|
||||
|
||||
verify(visibilityChecker, never()).canAccess(any(), any(), anyMap());
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) throws Exception {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue