mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
cli fix
This commit is contained in:
parent
6bbc82837b
commit
bd83f91648
27 changed files with 129 additions and 96 deletions
|
|
@ -50,7 +50,7 @@ ClawHub CLI 使用单一 slug 模型,slug 校验规则为 `[a-z0-9]([a-z0-9-]*
|
|||
- skillhub 自有 CLI 支持两种格式输入,内部统一转换为 namespace 坐标
|
||||
|
||||
**Well-known 发现:**
|
||||
- skillhub 服务端提供 `/.well-known/clawhub.json`,返回 `{ "apiBase": "/api/compat/v1" }`
|
||||
- skillhub 服务端提供 `/.well-known/clawhub.json`,返回 `{ "apiBase": "/api/v1" }`
|
||||
- ClawHub CLI 通过此机制自动发现兼容层 API 基地址
|
||||
|
||||
## 2. 参考项目取舍
|
||||
|
|
|
|||
|
|
@ -458,7 +458,7 @@ Session 中存储以下字段:
|
|||
- 后端设置 `XSRF-TOKEN` Cookie(`HttpOnly=false`)
|
||||
- 前端从 Cookie 读取 Token,放入请求 Header `X-XSRF-TOKEN`
|
||||
- 后端校验 Header 与 Cookie 是否一致
|
||||
- CLI API(`/api/v1/**`)与兼容层(`/api/compat/v1/**`)豁免 CSRF(使用 Bearer Token,无 Cookie)
|
||||
- CLI API(`/api/v1/**`)与兼容层(`/api/v1/**`)豁免 CSRF(使用 Bearer Token,无 Cookie)
|
||||
|
||||
## 9. 前端权限控制
|
||||
|
||||
|
|
@ -639,8 +639,8 @@ window.location.href = '/oauth2/authorization/github'
|
|||
|
||||
| 接口 | 所需凭证 | 额外判定 |
|
||||
|------|---------|---------|
|
||||
| `GET /api/compat/v1/whoami` | 任意有效 Bearer Token | 无 |
|
||||
| `GET /api/compat/v1/search` | 可选(匿名限 PUBLIC) | `SearchVisibilityScope` |
|
||||
| `GET /api/compat/v1/resolve` | 可选(匿名限 PUBLIC) | visibility |
|
||||
| `GET /api/compat/v1/download/{slug}/{version}` | 可选(匿名限 PUBLIC) | visibility |
|
||||
| `POST /api/compat/v1/publish` | Bearer Token + `skill:publish` | 用户是目标 namespace 的 MEMBER 以上(namespace 由 canonical slug 解析) |
|
||||
| `GET /api/v1/whoami` | 任意有效 Bearer Token | 无 |
|
||||
| `GET /api/v1/search` | 可选(匿名限 PUBLIC) | `SearchVisibilityScope` |
|
||||
| `GET /api/v1/resolve` | 可选(匿名限 PUBLIC) | visibility |
|
||||
| `GET /api/v1/download/{slug}/{version}` | 可选(匿名限 PUBLIC) | visibility |
|
||||
| `POST /api/v1/publish` | Bearer Token + `skill:publish` | 用户是目标 namespace 的 MEMBER 以上(namespace 由 canonical slug 解析) |
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
- 审核通过后转为 `PUBLISHED`
|
||||
- 审核拒绝后转为 `REJECTED`
|
||||
- 例外:提交人持有 `SUPER_ADMIN` 平台角色时,发布入口直接创建 `skill_version(status=PUBLISHED)`,跳过 `review_task` 创建,同时不再要求其必须是目标 namespace 成员
|
||||
- 上述例外必须对 Web、`/api/v1/publish`、`/api/compat/v1/publish` 保持一致
|
||||
- 上述例外必须对 Web、`/api/v1/publish`、`/api/v1/publish` 保持一致
|
||||
|
||||
### 对象存储写入策略
|
||||
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ Public API 的可见性规则:
|
|||
发布状态约束:
|
||||
|
||||
- 普通用户发布成功后,`status` 为 `PENDING_REVIEW`
|
||||
- 持有 `SUPER_ADMIN` 的用户通过 Web、`/api/v1/publish`、`/api/compat/v1/publish` 发布时,`status` 为 `PUBLISHED`,且不要求其必须是目标 namespace 成员
|
||||
- 持有 `SUPER_ADMIN` 的用户通过 Web、`/api/v1/publish`、`/api/v1/publish` 发布时,`status` 为 `PUBLISHED`,且不要求其必须是目标 namespace 成员
|
||||
- 当前版本保持该审核策略,不再提供“全员直发”的运行模式
|
||||
|
||||
## 7.4 Token API(需登录)
|
||||
|
|
@ -387,7 +387,7 @@ Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN:
|
|||
|
||||
## 7.10 ClawHub CLI 兼容层 API
|
||||
|
||||
兼容层 API 基地址为 `/api/compat/v1`,通过 `/.well-known/clawhub.json` 发现。兼容层使用 canonical slug(双连字符映射规则,详见 `00-product-direction.md` 1.1 节)。
|
||||
兼容层 API 基地址为 `/api/v1`,通过 `/.well-known/clawhub.json` 发现。兼容层使用 canonical slug(双连字符映射规则,详见 `00-product-direction.md` 1.1 节)。
|
||||
|
||||
认证方式:`Authorization: Bearer <token>`。Bearer Token 可来自 CLI Device Flow 或平台 API Token。
|
||||
|
||||
|
|
@ -398,7 +398,7 @@ GET /.well-known/clawhub.json
|
|||
|
||||
响应:
|
||||
{
|
||||
"apiBase": "/api/compat/v1"
|
||||
"apiBase": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -406,15 +406,15 @@ GET /.well-known/clawhub.json
|
|||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/compat/v1/whoami` | 当前用户信息 |
|
||||
| GET | `/api/compat/v1/search` | 搜索技能 |
|
||||
| GET | `/api/compat/v1/resolve` | 通过 slug + version 解析版本 |
|
||||
| GET | `/api/compat/v1/download/{slug}/{version}` | 下载技能 zip 包 |
|
||||
| POST | `/api/compat/v1/publish` | 发布技能(multipart/form-data,`SUPER_ADMIN` 直发) |
|
||||
| GET | `/api/v1/whoami` | 当前用户信息 |
|
||||
| GET | `/api/v1/search` | 搜索技能 |
|
||||
| GET | `/api/v1/resolve` | 通过 slug + version 解析版本 |
|
||||
| GET | `/api/v1/download/{slug}/{version}` | 下载技能 zip 包 |
|
||||
| POST | `/api/v1/publish` | 发布技能(multipart/form-data,`SUPER_ADMIN` 直发) |
|
||||
|
||||
### 兼容层请求/响应格式
|
||||
|
||||
**GET `/api/compat/v1/whoami`**
|
||||
**GET `/api/v1/whoami`**
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -424,7 +424,7 @@ GET /.well-known/clawhub.json
|
|||
}
|
||||
```
|
||||
|
||||
**GET `/api/compat/v1/search?q={keyword}&page={page}&limit={limit}`**
|
||||
**GET `/api/v1/search?q={keyword}&page={page}&limit={limit}`**
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -449,21 +449,21 @@ GET /.well-known/clawhub.json
|
|||
|
||||
注意:兼容层返回的 `slug` 为 canonical slug 格式(全局空间直接返回 skill slug,团队空间返回 `namespace--skill`)。
|
||||
|
||||
**GET `/api/compat/v1/resolve?slug={slug}&version={version}`**
|
||||
**GET `/api/v1/resolve?slug={slug}&version={version}`**
|
||||
|
||||
```json
|
||||
{
|
||||
"slug": "my-skill",
|
||||
"version": "1.2.0",
|
||||
"downloadUrl": "/api/compat/v1/download/my-skill/1.2.0"
|
||||
"downloadUrl": "/api/v1/download/my-skill/1.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
**GET `/api/compat/v1/download/{slug}/{version}`**
|
||||
**GET `/api/v1/download/{slug}/{version}`**
|
||||
|
||||
返回指定版本的 zip 文件流。默认版本解析由 `resolve` 接口负责。
|
||||
|
||||
**POST `/api/compat/v1/publish`**
|
||||
**POST `/api/v1/publish`**
|
||||
|
||||
```
|
||||
Content-Type: multipart/form-data
|
||||
|
|
@ -521,7 +521,7 @@ Parts:
|
|||
- 版本策略:URL path 版本 `/api/v1/`
|
||||
- 幂等性:写操作通过 `X-Request-Id` + Redis 去重(TTL 24h)
|
||||
|
||||
### Compatibility API(`/api/compat/v1/*`)
|
||||
### Compatibility API(`/api/v1/*`)
|
||||
|
||||
- 响应格式完全遵循 ClawHub 协议,不套统一响应包裹
|
||||
- 错误响应遵循 ClawHub 格式:`{ error: string, message: string }`
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
- 一期同步发布模型,暂不考虑异步发布
|
||||
- API Token 一期继承用户全部权限(非最小权限),后续版本细化
|
||||
- CLI 主认证切换为 OAuth Device Flow,Bearer Token 统一用于 CLI API 和兼容层
|
||||
- ClawHub CLI 兼容层基地址 `/api/compat/v1`,通过 `/.well-known/clawhub.json` 发现
|
||||
- ClawHub CLI 兼容层基地址 `/api/v1`,通过 `/.well-known/clawhub.json` 发现
|
||||
|
||||
## Phase 1:工程骨架 + 认证打通
|
||||
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
- 评分 + 收藏 + 计数器(原子更新)
|
||||
- OAuth Device Flow(device code、授权确认、轮询换取 Bearer Token)
|
||||
- CLI API(whoami、publish、resolve、check)
|
||||
- ClawHub CLI 协议兼容层(`/api/compat/v1` 端点:search、resolve、download、publish、whoami)
|
||||
- ClawHub CLI 协议兼容层(`/api/v1` 端点:search、resolve、download、publish、whoami)
|
||||
- 兼容层 canonical slug 映射(`--` 双连字符规则)
|
||||
- `/.well-known/clawhub.json` 发现端点
|
||||
- 协议适配器与兼容性测试(针对 ClawHub CLI 的真实请求/响应样例)
|
||||
|
|
|
|||
|
|
@ -2756,7 +2756,7 @@ public class ApiTokenAuthenticationFilter extends OncePerRequestFilter {
|
|||
// 仅对 CLI 和 Token API 路径生效
|
||||
String path = request.getRequestURI();
|
||||
return !(path.startsWith("/api/v1/cli/") || path.startsWith("/api/v1/tokens")
|
||||
|| path.startsWith("/api/compat/"));
|
||||
|| path.startsWith("/api/"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -2974,7 +2974,7 @@ public class SecurityConfig {
|
|||
.csrf(csrf -> csrf
|
||||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
||||
.csrfTokenRequestHandler(csrfHandler)
|
||||
.ignoringRequestMatchers("/api/v1/cli/**", "/api/compat/**")
|
||||
.ignoringRequestMatchers("/api/v1/cli/**", "/api/**")
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 公开端点
|
||||
|
|
|
|||
|
|
@ -6709,7 +6709,7 @@ export function FileTree({ files, onSelect }: {
|
|||
import { CopyButton } from '@/shared/components/copy-button';
|
||||
|
||||
export function InstallCommand({ namespace, slug }: { namespace: string; slug: string }) {
|
||||
const command = `skillhub install @${namespace}/${slug}`;
|
||||
const command = `clawhub install @${namespace}/${slug}`;
|
||||
return (
|
||||
<div className="flex items-center gap-2 bg-muted rounded-lg px-3 py-2">
|
||||
<code className="text-sm flex-1 font-mono truncate">{command}</code>
|
||||
|
|
|
|||
|
|
@ -6428,7 +6428,7 @@ class WellKnownControllerTest {
|
|||
void returns_api_base() throws Exception {
|
||||
mockMvc.perform(get("/.well-known/clawhub.json"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.apiBase").value("/api/compat/v1"));
|
||||
.andExpect(jsonPath("$.apiBase").value("/api/v1"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -6446,7 +6446,7 @@ import java.util.Map;
|
|||
public class WellKnownController {
|
||||
@GetMapping("/.well-known/clawhub.json")
|
||||
public Map<String, String> clawHubDiscovery() {
|
||||
return Map.of("apiBase", "/api/compat/v1");
|
||||
return Map.of("apiBase", "/api/v1");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -6527,7 +6527,7 @@ class ClawHubCompatControllerTest {
|
|||
|
||||
@Test
|
||||
void search_returns_compat_format() throws Exception {
|
||||
mockMvc.perform(get("/api/compat/v1/search").param("q", "test"))
|
||||
mockMvc.perform(get("/api/v1/search").param("q", "test"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items").isArray());
|
||||
}
|
||||
|
|
@ -6536,14 +6536,14 @@ class ClawHubCompatControllerTest {
|
|||
void resolve_parses_canonical_slug() throws Exception {
|
||||
when(slugMapper.fromCanonical("my-skill"))
|
||||
.thenReturn(new SkillCoordinate("global", "my-skill"));
|
||||
mockMvc.perform(get("/api/compat/v1/resolve")
|
||||
mockMvc.perform(get("/api/v1/resolve")
|
||||
.param("slug", "my-skill"))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whoami_requires_auth() throws Exception {
|
||||
mockMvc.perform(get("/api/compat/v1/whoami"))
|
||||
mockMvc.perform(get("/api/v1/whoami"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
|
|
@ -6566,7 +6566,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/compat/v1")
|
||||
@RequestMapping("/api/v1")
|
||||
public class ClawHubCompatController {
|
||||
private final CanonicalSlugMapper slugMapper;
|
||||
|
||||
|
|
@ -6591,7 +6591,7 @@ public class ClawHubCompatController {
|
|||
SkillCoordinate coord = slugMapper.fromCanonical(slug);
|
||||
// TODO: 调用 SkillQueryService 获取版本详情
|
||||
return new ClawHubResolveResponse(slug, "", version,
|
||||
"/api/compat/v1/download/" + slug + "/" + version, 0, 0);
|
||||
"/api/v1/download/" + slug + "/" + version, 0, 0);
|
||||
}
|
||||
|
||||
@GetMapping("/download/{slug}/{version}")
|
||||
|
|
|
|||
|
|
@ -1016,14 +1016,14 @@ GET /.well-known/clawhub.json
|
|||
|
||||
Response 200:
|
||||
{
|
||||
"apiBase": "/api/compat/v1"
|
||||
"apiBase": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
#### search - 搜索技能
|
||||
|
||||
```
|
||||
GET /api/compat/v1/search?q=keyword&page=0&size=20
|
||||
GET /api/v1/search?q=keyword&page=0&size=20
|
||||
Authorization: Bearer sk_xxx(可选)
|
||||
|
||||
Response 200:
|
||||
|
|
@ -1057,7 +1057,7 @@ Response 200:
|
|||
#### resolve - 解析技能版本
|
||||
|
||||
```
|
||||
GET /api/compat/v1/resolve?slug=my-skill&version=1.2.0
|
||||
GET /api/v1/resolve?slug=my-skill&version=1.2.0
|
||||
Authorization: Bearer sk_xxx(可选)
|
||||
|
||||
Response 200:
|
||||
|
|
@ -1065,7 +1065,7 @@ Response 200:
|
|||
"slug": "my-skill",
|
||||
"name": "My Skill",
|
||||
"version": "1.2.0",
|
||||
"downloadUrl": "/api/compat/v1/download/my-skill/1.2.0",
|
||||
"downloadUrl": "/api/v1/download/my-skill/1.2.0",
|
||||
"fileCount": 5,
|
||||
"totalSize": 12345
|
||||
}
|
||||
|
|
@ -1079,7 +1079,7 @@ Response 200:
|
|||
#### download - 下载技能包
|
||||
|
||||
```
|
||||
GET /api/compat/v1/download/{slug}/{version}
|
||||
GET /api/v1/download/{slug}/{version}
|
||||
Authorization: Bearer sk_xxx(可选)
|
||||
|
||||
Response 200:
|
||||
|
|
@ -1097,7 +1097,7 @@ Content-Disposition: attachment; filename="my-skill-1.2.0.zip"
|
|||
#### publish - 发布技能
|
||||
|
||||
```
|
||||
POST /api/compat/v1/publish
|
||||
POST /api/v1/publish
|
||||
Authorization: Bearer sk_xxx
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
|
|
@ -1118,7 +1118,7 @@ Response 200:
|
|||
#### whoami - 查询当前用户
|
||||
|
||||
```
|
||||
GET /api/compat/v1/whoami
|
||||
GET /api/v1/whoami
|
||||
Authorization: Bearer sk_xxx
|
||||
|
||||
Response 200:
|
||||
|
|
@ -1135,7 +1135,7 @@ Response 200:
|
|||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/compat/v1")
|
||||
@RequestMapping("/api/v1")
|
||||
public class ClawHubCompatController {
|
||||
|
||||
private final CanonicalSlugMapper slugMapper;
|
||||
|
|
@ -1193,7 +1193,7 @@ public class ClawHubCompatController {
|
|||
slug,
|
||||
detail.displayName(),
|
||||
detail.version(),
|
||||
"/api/compat/v1/download/" + slug + "/" + detail.version(),
|
||||
"/api/v1/download/" + slug + "/" + detail.version(),
|
||||
detail.fileCount(),
|
||||
detail.totalSize()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ description: 安装和使用技能
|
|||
### 安装最新版本
|
||||
|
||||
```bash
|
||||
skillhub install @team/my-skill
|
||||
clawhub install @team/my-skill
|
||||
```
|
||||
|
||||
### 安装指定版本
|
||||
|
||||
```bash
|
||||
skillhub install @team/my-skill@1.2.0
|
||||
clawhub install @team/my-skill@1.2.0
|
||||
```
|
||||
|
||||
### 按标签安装
|
||||
|
||||
```bash
|
||||
skillhub install @team/my-skill@beta
|
||||
clawhub install @team/my-skill@beta
|
||||
```
|
||||
|
||||
### 使用 ClawHub CLI 安装
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ description: 技能评分和收藏功能
|
|||
|
||||
查看已收藏的技能:
|
||||
- Web:进入"我的收藏"
|
||||
- CLI:`skillhub stars`
|
||||
- CLI:`clawhub stars`
|
||||
|
||||
## 技能评分
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ description: 发布技能到 SkillHub
|
|||
|
||||
```bash
|
||||
# 1. 登录
|
||||
skillhub login
|
||||
clawhub login
|
||||
|
||||
# 2. 发布
|
||||
skillhub publish ./my-skill.zip --namespace @team-myteam
|
||||
clawhub publish ./my-skill.zip --namespace @team-myteam
|
||||
```
|
||||
|
||||
## 通过 ClawHub CLI 发布
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ SkillHub 使用语义化版本(Semantic Versioning):`MAJOR.MINOR.PATCH`
|
|||
### 创建/移动标签
|
||||
|
||||
```bash
|
||||
skillhub tag set @team/my-skill beta 1.2.0
|
||||
clawhub tag set @team/my-skill beta 1.2.0
|
||||
```
|
||||
|
||||
### 删除标签
|
||||
|
||||
```bash
|
||||
skillhub tag delete @team/my-skill beta
|
||||
clawhub tag delete @team/my-skill beta
|
||||
```
|
||||
|
||||
## 版本撤回
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ GET /.well-known/clawhub.json
|
|||
|
||||
```json
|
||||
{
|
||||
"apiBase": "/api/compat/v1"
|
||||
"apiBase": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ GET /.well-known/clawhub.json
|
|||
### Whoami
|
||||
|
||||
```http
|
||||
GET /api/compat/v1/whoami
|
||||
GET /api/v1/whoami
|
||||
```
|
||||
|
||||
响应:
|
||||
|
|
@ -43,7 +43,7 @@ GET /api/compat/v1/whoami
|
|||
### 搜索
|
||||
|
||||
```http
|
||||
GET /api/compat/v1/search?q={keyword}&page={page}&limit={limit}
|
||||
GET /api/v1/search?q={keyword}&page={page}&limit={limit}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
|
@ -75,7 +75,7 @@ GET /api/compat/v1/search?q={keyword}&page={page}&limit={limit}
|
|||
### 解析
|
||||
|
||||
```http
|
||||
GET /api/compat/v1/resolve?slug={slug}&version={version}
|
||||
GET /api/v1/resolve?slug={slug}&version={version}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
|
@ -84,20 +84,20 @@ GET /api/compat/v1/resolve?slug={slug}&version={version}
|
|||
{
|
||||
"slug": "my-skill",
|
||||
"version": "1.2.0",
|
||||
"downloadUrl": "/api/compat/v1/download/my-skill/1.2.0"
|
||||
"downloadUrl": "/api/v1/download/my-skill/1.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
### 下载
|
||||
|
||||
```http
|
||||
GET /api/compat/v1/download/{slug}/{version}
|
||||
GET /api/v1/download/{slug}/{version}
|
||||
```
|
||||
|
||||
### 发布
|
||||
|
||||
```http
|
||||
POST /api/compat/v1/publish
|
||||
POST /api/v1/publish
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <zip-file>
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ description: Install and use skills
|
|||
### Install Latest Version
|
||||
|
||||
```bash
|
||||
skillhub install @team/my-skill
|
||||
clawhub install @team/my-skill
|
||||
```
|
||||
|
||||
### Install Specific Version
|
||||
|
||||
```bash
|
||||
skillhub install @team/my-skill@1.2.0
|
||||
clawhub install @team/my-skill@1.2.0
|
||||
```
|
||||
|
||||
### Install by Tag
|
||||
|
||||
```bash
|
||||
skillhub install @team/my-skill@beta
|
||||
clawhub install @team/my-skill@beta
|
||||
```
|
||||
|
||||
### Install with ClawHub CLI
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Click the "Favorite" button on the skill detail page to favorite a skill.
|
|||
|
||||
View your favorite skills:
|
||||
- Web: Go to "My Favorites"
|
||||
- CLI: `skillhub stars`
|
||||
- CLI: `clawhub stars`
|
||||
|
||||
## Skill Rating
|
||||
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ You can create custom tags for version channel management:
|
|||
### Create/Move Tag
|
||||
|
||||
```bash
|
||||
skillhub tag set @team/my-skill beta 1.2.0
|
||||
clawhub tag set @team/my-skill beta 1.2.0
|
||||
```
|
||||
|
||||
### Delete Tag
|
||||
|
||||
```bash
|
||||
skillhub tag delete @team/my-skill beta
|
||||
clawhub tag delete @team/my-skill beta
|
||||
```
|
||||
|
||||
## Version Withdrawal
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Response:
|
|||
|
||||
```json
|
||||
{
|
||||
"apiBase": "/api/compat/v1"
|
||||
"apiBase": "/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ Response:
|
|||
### Whoami
|
||||
|
||||
```http
|
||||
GET /api/compat/v1/whoami
|
||||
GET /api/v1/whoami
|
||||
```
|
||||
|
||||
Response:
|
||||
|
|
@ -43,7 +43,7 @@ Response:
|
|||
### Search
|
||||
|
||||
```http
|
||||
GET /api/compat/v1/search?q={keyword}&page={page}&limit={limit}
|
||||
GET /api/v1/search?q={keyword}&page={page}&limit={limit}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
|
@ -75,7 +75,7 @@ Response:
|
|||
### Resolve
|
||||
|
||||
```http
|
||||
GET /api/compat/v1/resolve?slug={slug}&version={version}
|
||||
GET /api/v1/resolve?slug={slug}&version={version}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
|
@ -84,20 +84,20 @@ Response:
|
|||
{
|
||||
"slug": "my-skill",
|
||||
"version": "1.2.0",
|
||||
"downloadUrl": "/api/compat/v1/download/my-skill/1.2.0"
|
||||
"downloadUrl": "/api/v1/download/my-skill/1.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Download
|
||||
|
||||
```http
|
||||
GET /api/compat/v1/download/{slug}/{version}
|
||||
GET /api/v1/download/{slug}/{version}
|
||||
```
|
||||
|
||||
### Publish
|
||||
|
||||
```http
|
||||
POST /api/compat/v1/publish
|
||||
POST /api/v1/publish
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <zip-file>
|
||||
|
|
|
|||
|
|
@ -188,8 +188,8 @@ public class ClawHubCompatController {
|
|||
}
|
||||
|
||||
@GetMapping("/download/{canonicalSlug}")
|
||||
public ResponseEntity<Void> download(@PathVariable String canonicalSlug,
|
||||
@RequestParam(defaultValue = "latest") String version) {
|
||||
public ResponseEntity<Void> downloadByPath(@PathVariable String canonicalSlug,
|
||||
@RequestParam(defaultValue = "latest") String version) {
|
||||
SkillCoordinate coord = mapper.fromCanonical(canonicalSlug);
|
||||
String location = "latest".equals(version)
|
||||
? "/api/v1/skills/" + coord.namespace() + "/" + coord.slug() + "/download"
|
||||
|
|
@ -199,6 +199,22 @@ public class ClawHubCompatController {
|
|||
.build();
|
||||
}
|
||||
|
||||
@GetMapping("/download")
|
||||
public ResponseEntity<Void> downloadByQuery(@RequestParam String slug,
|
||||
@RequestParam(defaultValue = "latest") String version) {
|
||||
// For query param version, slug is just the skill slug without namespace
|
||||
Skill skill = skillRepository.findBySlug(slug).stream().findFirst()
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.skill.notFound", slug));
|
||||
Namespace ns = namespaceRepository.findById(skill.getNamespaceId())
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.namespace.notFound", skill.getNamespaceId()));
|
||||
String location = "latest".equals(version)
|
||||
? "/api/v1/skills/" + ns.getSlug() + "/" + skill.getSlug() + "/download"
|
||||
: "/api/v1/skills/" + ns.getSlug() + "/" + skill.getSlug() + "/versions/" + version + "/download";
|
||||
return ResponseEntity.status(HttpStatus.FOUND)
|
||||
.header(HttpHeaders.LOCATION, location)
|
||||
.build();
|
||||
}
|
||||
|
||||
@GetMapping("/skills")
|
||||
public ClawHubSearchResponse listSkills(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
|
|
@ -269,7 +285,7 @@ public class ClawHubCompatController {
|
|||
versionInfo = new ClawHubSkillResponse.VersionInfo(
|
||||
latestVersionEntity.getVersion(),
|
||||
versionCreatedAt,
|
||||
latestVersionEntity.getChangelog(),
|
||||
latestVersionEntity.getChangelog() == null ? "" : latestVersionEntity.getChangelog(),
|
||||
null // license
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@ public class RequestLoggingFilter extends OncePerRequestFilter {
|
|||
}
|
||||
|
||||
sb.append("Response Status: ").append(response.getStatus()).append("\n");
|
||||
|
||||
String responseBody = getResponseBody(response);
|
||||
if (responseBody != null && !responseBody.isBlank()) {
|
||||
sb.append("Response Body: ").append(responseBody).append("\n");
|
||||
}
|
||||
|
||||
sb.append("Duration: ").append(duration).append("ms\n");
|
||||
sb.append("===================================");
|
||||
|
||||
|
|
@ -87,4 +93,16 @@ public class RequestLoggingFilter extends OncePerRequestFilter {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getResponseBody(ContentCachingResponseWrapper response) {
|
||||
byte[] buf = response.getContentAsByteArray();
|
||||
if (buf.length > 0) {
|
||||
try {
|
||||
return new String(buf, response.getCharacterEncoding());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return "[unknown encoding]";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@ class WellKnownControllerTest {
|
|||
void clawhubConfig_returns_apiBase() throws Exception {
|
||||
mockMvc.perform(get("/.well-known/clawhub.json"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.apiBase").value("/api/compat/v1"));
|
||||
.andExpect(jsonPath("$.apiBase").value("/api/v1"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ public class SecurityConfig {
|
|||
if (path == null) {
|
||||
return false;
|
||||
}
|
||||
return path.startsWith("/api/compat/")
|
||||
return path.startsWith("/api/")
|
||||
|| path.equals("/api/v1/publish")
|
||||
|| path.startsWith("/api/v1/auth/device/");
|
||||
};
|
||||
|
|
@ -117,9 +117,9 @@ public class SecurityConfig {
|
|||
"/v3/api-docs/**",
|
||||
"/swagger-ui/**",
|
||||
"/.well-known/**",
|
||||
"/api/compat/v1/search",
|
||||
"/api/compat/v1/resolve/**",
|
||||
"/api/compat/v1/download/**"
|
||||
"/api/v1/search",
|
||||
"/api/v1/resolve/**",
|
||||
"/api/v1/download/**"
|
||||
).permitAll()
|
||||
.requestMatchers("/actuator/prometheus").hasAnyRole("SUPER_ADMIN", "AUDITOR")
|
||||
.requestMatchers(
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ public class ApiTokenAuthenticationFilter extends OncePerRequestFilter {
|
|||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getRequestURI();
|
||||
return !(path.startsWith("/api/v1/")
|
||||
|| path.startsWith("/api/web/")
|
||||
|| path.startsWith("/api/compat/"));
|
||||
|| path.startsWith("/api/web/"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,8 +67,7 @@ public class ApiTokenScopeFilter extends OncePerRequestFilter {
|
|||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getRequestURI();
|
||||
return path == null || (!path.startsWith("/api/v1/")
|
||||
&& !path.startsWith("/api/web/")
|
||||
&& !path.startsWith("/api/compat/"));
|
||||
&& !path.startsWith("/api/web/"));
|
||||
}
|
||||
|
||||
private boolean isApiTokenAuthentication(Authentication authentication) {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,10 @@ public class ApiTokenScopeService {
|
|||
ScopeRule.allow("GET", "/api/v1/namespaces/*"),
|
||||
ScopeRule.allow("GET", "/api/web/namespaces"),
|
||||
ScopeRule.allow("GET", "/api/web/namespaces/*"),
|
||||
ScopeRule.allow("GET", "/api/compat/v1/search"),
|
||||
ScopeRule.allow("GET", "/api/compat/v1/resolve/**"),
|
||||
ScopeRule.allow("GET", "/api/compat/v1/whoami"),
|
||||
ScopeRule.allow("GET", "/api/v1/search"),
|
||||
ScopeRule.allow("GET", "/api/v1/resolve/**"),
|
||||
ScopeRule.allow("GET", "/api/v1/whoami"),
|
||||
ScopeRule.allow("GET", "/api/v1/download"),
|
||||
ScopeRule.allow(null, "/.well-known/**"),
|
||||
ScopeRule.allow(null, "/actuator/health"),
|
||||
ScopeRule.allow(null, "/v3/api-docs/**"),
|
||||
|
|
@ -47,7 +48,7 @@ public class ApiTokenScopeService {
|
|||
ScopeRule.require("POST", "/api/v1/skills/*/publish", "skill:publish"),
|
||||
ScopeRule.require("POST", "/api/web/skills/*/publish", "skill:publish"),
|
||||
ScopeRule.require("POST", "/api/v1/publish", "skill:publish"),
|
||||
ScopeRule.require("POST", "/api/compat/v1/publish", "skill:publish")
|
||||
ScopeRule.require("POST", "/api/v1/publish", "skill:publish")
|
||||
);
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
|
@ -103,7 +104,7 @@ public class ApiTokenScopeService {
|
|||
}
|
||||
|
||||
private boolean isApiPath(String path) {
|
||||
return path != null && (path.startsWith("/api/v1/") || path.startsWith("/api/web/") || path.startsWith("/api/compat/"));
|
||||
return path != null && (path.startsWith("/api/v1/") || path.startsWith("/api/web/") || path.startsWith("/api/"));
|
||||
}
|
||||
|
||||
public record AuthorizationDecision(boolean allowed, String requiredScope, String message) {
|
||||
|
|
|
|||
10
web/src/api/generated/schema.d.ts
vendored
10
web/src/api/generated/schema.d.ts
vendored
|
|
@ -564,7 +564,7 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/compat/v1/publish": {
|
||||
"/api/v1/publish": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
|
@ -1012,7 +1012,7 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/compat/v1/whoami": {
|
||||
"/api/v1/whoami": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
|
@ -1028,7 +1028,7 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/compat/v1/search": {
|
||||
"/api/v1/search": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
|
@ -1044,7 +1044,7 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/compat/v1/resolve/{canonicalSlug}": {
|
||||
"/api/v1/resolve/{canonicalSlug}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
|
@ -1060,7 +1060,7 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/compat/v1/download/{canonicalSlug}": {
|
||||
"/api/v1/download/{canonicalSlug}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ export function InstallCommand({ namespace, slug, version }: InstallCommandProps
|
|||
const { t } = useTranslation()
|
||||
const [copied, setCopied] = useState(false)
|
||||
const command = version
|
||||
? `skillhub install ${namespace}/${slug}@${version}`
|
||||
: `skillhub install ${namespace}/${slug}`
|
||||
? `clawhub install ${namespace}/${slug}@${version}`
|
||||
: `clawhub install ${namespace}/${slug}`
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue