skillhub/docs/06-api-design.md
thiagonogueira 217e7f4042
Some checks are pending
Deploy Docs / build (push) Waiting to run
Deploy Docs / Deploy (push) Blocked by required conditions
Security / Dependency Review (push) Waiting to run
Security / CodeQL (java-kotlin) (push) Waiting to run
Security / CodeQL (python) (push) Waiting to run
Security / CodeQL (javascript-typescript) (push) Waiting to run
feat(skills): let skill owners yank a published version (#866)
* feat(auth): expose skill lifecycle routes to API tokens

With an API token, v0.2.19 can remove a whole skill (DELETE
/api/v1/skills/{ns}/{slug} with skill:delete) but cannot archive or
unarchive a skill, nor delete a single draft/rejected version. Those
three routes are opened by AUTHORIZATION_POLICIES (authenticated
fallback) yet have no entry in API_TOKEN_POLICIES, so a bearer request
falls through to "unsupported" and is rejected with 403.

That contradicts the contract written above SESSION_ONLY_ROUTES in
RouteSecurityPolicyRegistry: bearer tokens are rejected on exactly the
listed session-only routes and nowhere else, and anything else the
authorization list opens must be reachable with a token holding the
required scope.

Add API-token policies for both the /api/v1 and /api/web prefixes that
SkillLifecycleController serves:

- POST .../skills/{ns}/{slug}/archive and .../unarchive require
  skill:publish. They are owner-level operations, gated by the same
  assertCanManageLifecycle check as publishing, so they sit at the same
  scope tier.
- DELETE .../skills/{ns}/{slug}/versions/{version} requires
  skill:delete, matching the existing whole-skill delete.

Whole-skill DELETE on /api/web stays session-only as documented; the
new version-delete pattern does not overlap it. No scope allow-list
exists outside the registry (TokenController and ApiTokenScopeService
accept any scope string), so no other change is needed for tokens to
carry these scopes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdnYX4jTS3JwMMP9JCGxzU

* feat(skills): let skill owners yank a published version

Yanking a published version is only available through
POST /api/v1/admin/skills/versions/{versionId}/yank, which is
session-only (SESSION_ONLY_ROUTES covers /api/v1/admin/**) and requires
SKILL_ADMIN or SUPER_ADMIN. A skill owner therefore cannot pull a
broken release themselves, neither from the web surface nor from a
script holding an API token.

In package registries yank is an act of the publisher: `cargo yank`
and PyPI's "yank release" are performed by the package owner, not by a
registry admin, because the goal is to stop new installs of a bad
release while keeping the artifact available for lock files. SkillHub
already lets owners archive, unarchive, rerelease and delete draft
versions through SkillLifecycleController under the
assertCanManageLifecycle rule (owner, or namespace ADMIN/OWNER); yank
belongs on the same surface with the same rule.

Changes:

- SkillGovernanceService: add an owner-checked yankVersion(skill,
  version, actor, roles, ip, ua, reason) that runs
  assertCanManageLifecycle and then the same yank logic as the admin
  variant, now shared in yankVersionInternal. The admin entry point is
  unchanged for AdminSkillController.
- SkillLifecycleAppService / GovernanceWorkflowAppService: resolve
  skill and version by namespace/slug/version, delegate to the new
  domain method, and return SkillLifecycleMutationResponse with action
  YANK and the resulting version status. The YANK_SKILL_VERSION audit
  record and SkillVersionYankedEvent are emitted by the domain service
  exactly as for the admin path.
- SkillLifecycleController: POST /{namespace}/{slug}/versions/{version}/yank
  on both /api/v1/skills and /api/web/skills, optional body
  AdminSkillActionRequest (reason).
- RouteSecurityPolicyRegistry: require skill:yank for the new route on
  both prefixes, so tokens can reach it as the SESSION_ONLY_ROUTES
  comment promises for every route the authorization list opens. The
  admin yank stays session-only. No allow-list of scopes exists outside
  the registry; the docs' scope enumeration is updated to include
  skill:yank.
- Tests: RouteSecurityPolicyRegistryTest (scope required on both
  prefixes, admin route still closed), SkillGovernanceServiceTest
  (owner and namespace ADMIN allowed, MEMBER forbidden, unpublished
  rejected), SkillLifecycleControllerTest (envelope with and without
  body).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdnYX4jTS3JwMMP9JCGxzU

* fix(auth): complete API token lifecycle access

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(skills): align owner lifecycle token access

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

---------

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: Thiago Nascimento Nogueira <thiago.nascimento.nogueira@emeal.nttdata.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
2026-09-17 18:48:05 +08:00

23 KiB
Raw Blame History

skillhub API 设计

0. 标识类型约束

  • 所有 API 中出现的用户标识一律为 string。
  • 该约束覆盖路径参数、query 参数、请求体字段、响应 DTO 字段,以及统一响应结构中的业务数据内容。
  • 任何旧草案中的整型用户标识写法都已失效,前后端正式契约只允许字符串用户标识。

1. 响应结构规范

除文件下载、文件内容读取这类二进制流接口外,所有 JSON API 必须统一使用以下成功响应结构:

{
  "code": 0,
  "msg": "成功",
  "data": {},
  "timestamp": "2026-03-12T06:00:00Z",
  "requestId": "req-123"
}

约束如下:

  • code:成功时固定为 0;失败时固定为 HTTP 状态码,例如 400、401、403、500。
  • msg:返回给调用方的用户可读提示文案,必须通过 Spring Boot MessageSource + i18n 机制生成,禁止在 controller 中硬编码。
  • msg 的 locale 必须在响应封装层或全局异常处理层通过 LocaleContextHolder 从请求上下文自动获取,禁止在 controller/service 中显式传递 Locale。
  • data 承载实际业务数据;列表、分页对象、详情对象、操作结果对象都必须放在 data 下。
  • 分页响应统一使用 { items, total, page, size },禁止直接暴露 Spring Page 的 content/pageable/sort/first/last 等内部结构。
  • timestamp:响应创建时间戳,由后端统一自动生成。
  • requestId:请求链路 ID,由后端统一注入,便于日志追踪。
  • Controller 层禁止直接返回 Map、裸 DTO、裸 Page、裸 List 作为 JSON 成功响应。
  • 普通 JSON 接口应直接返回统一响应 DTO;仅文件下载、文件预览等需要自定义状态码或 header 的二进制接口保留 ResponseEntity。
  • 删除、撤销、移动标签等操作也必须返回统一 JSON 结构;如无实体数据,返回 data.message 或 data=null,但外层结构不得变化。
  • 错误响应与成功响应使用同一外层结构,不再使用单独的异常 JSON 结构。
  • 异常链路中的 msg 也必须通过 Spring Boot 标准 i18n 机制生成;参数校验异常、领域异常、认证鉴权异常都必须进入统一的 @RestControllerAdvice 出口。
  • 二进制流接口保持原始 HTTP 语义,不套 code/data 包装:
    • /download
    • /file
    • 其他返回 application/octet-stream、application/zip 等内容类型的接口

成功响应示例:

{
  "code": 0,
  "msg": "发布成功",
  "data": {
    "skillId": 123,
    "version": "1.0.0"
  },
  "timestamp": "2026-03-12T06:00:00Z",
  "requestId": "req-123"
}

错误响应示例:

{
  "code": 403,
  "msg": "需要命名空间管理员或所有者权限",
  "data": null,
  "timestamp": "2026-03-12T06:00:00Z",
  "requestId": "req-123"
}

7.1 Public API(匿名可访问)

方法 路径 说明
GET /api/v1/skills 搜索/列表(匿名仅返回 PUBLIC 技能)
GET /api/v1/skills/{namespace}/{slug} 技能详情(PUBLIC 匿名可访问)
GET /api/v1/skills/{namespace}/{slug}/versions 版本列表
GET /api/v1/skills/{namespace}/{slug}/versions/{version} 版本详情
GET /api/v1/skills/{namespace}/{slug}/versions/{version}/files 文件清单
GET /api/v1/skills/{namespace}/{slug}/versions/{version}/file?path=... 读取单个文件(query param 避免路径中 / 的解析问题)
GET /api/v1/skills/{namespace}/{slug}/download 下载默认安装版本(最新已发布版本)
GET /api/v1/skills/{namespace}/{slug}/versions/{version}/download 下载指定版本包
GET /api/v1/skills/{namespace}/{slug}/resolve 解析技能版本(支持 query param: version、tag、hash)
GET /api/v1/skills/{namespace}/{slug}/tags/{tagName}/download 按标签下载(解析标签指向的版本后下载)
GET /api/v1/skills/{namespace}/{slug}/tags/{tagName}/files 按标签查看文件清单
GET /api/v1/skills/{namespace}/{slug}/tags/{tagName}/file?path=... 按标签读取单个文件
GET /api/v1/skills/{skillId}/reviews 公开评价分页列表;仅返回可见评价,管理员可见隐藏项
GET /api/v1/namespaces 公开命名空间列表
GET /api/v1/namespaces/{slug} 命名空间详情

Public API 的可见性规则:

  • PUBLIC 技能:若存在已发布版本,则已登录用户可访问;匿名访问仍受下载/resolve 端点的 namespace 类型限制
  • NAMESPACE_ONLY 技能:仅该命名空间成员可访问(需登录)
  • PRIVATE 技能:owner 本人 + 该 namespace 的 ADMIN 以上可访问(需登录)
  • 若 latest_version_id = null,即使 visibility=PUBLIC,skill 也不会对外公开,只有 owner 可访问
  • hidden=true 时,普通访客不可访问;仅 owner 或该 namespace 的 ADMIN / OWNER 可访问

GET /api/v1/skills/{namespace}/{slug}/versions/{version} 的 data 字段除版本基础信息外,还必须包含:

  • parsedMetadataJson:SKILL.md frontmatter 的完整 JSON 序列化结果
  • manifestJson:版本文件清单摘要 JSON

7.2 Auth API(登录与会话相关)

方法 路径 说明
GET /oauth2/authorization/github 发起 GitHub OAuth 登录(Spring Security 内置)
GET /login/oauth2/code/github GitHub OAuth 回调(Spring Security 内置)
GET /api/v1/auth/me 当前用户信息(未登录返回 401)
POST /api/v1/auth/logout 登出(清除 Session)
GET /api/v1/auth/providers 可用的 OAuth Provider 列表(前端渲染登录按钮用)
GET /api/v1/auth/methods 统一登录方式目录(密码/OAuth/direct/bootstrap 元数据)
POST /api/v1/auth/direct/login 显式走直连认证 provider 的兼容登录入口(默认关闭)
POST /api/v1/auth/session/bootstrap 显式尝试用外部被动会话换取 skillhub Session(默认关闭)

/api/v1/auth/providers 响应示例:

{
  "code": 0,
  "msg": "获取成功",
  "data": [
    { "id": "github", "name": "GitHub", "authorizationUrl": "/oauth2/authorization/github" }
  ],
  "timestamp": "2026-03-12T06:00:00Z",
  "requestId": "req-123"
}

前端根据此接口动态渲染登录按钮,新增 Provider 无需改前端代码。

/api/v1/auth/methods 返回统一登录方式目录。典型项包括:

  • PASSWORD:现有本地账号密码登录
  • OAUTH_REDIRECT:OAuth 跳转登录
  • DIRECT_PASSWORD:默认关闭的直连认证兼容入口
  • SESSION_BOOTSTRAP:默认关闭的被动会话引导入口

示例:

{
  "code": 0,
  "msg": "获取成功",
  "data": [
    {
      "id": "local-password",
      "methodType": "PASSWORD",
      "provider": "local",
      "displayName": "Local Account",
      "actionUrl": "/api/v1/auth/local/login"
    },
    {
      "id": "oauth-github",
      "methodType": "OAUTH_REDIRECT",
      "provider": "github",
      "displayName": "GitHub",
      "actionUrl": "/oauth2/authorization/github"
    }
  ],
  "timestamp": "2026-03-12T06:00:00Z",
  "requestId": "req-123"
}

/api/v1/auth/session/bootstrap 请求示例:

{
  "provider": "private-sso"
}

/api/v1/auth/session/bootstrap 协议约束:

  • 开源版默认关闭,需显式开启 skillhub.auth.session-bootstrap.enabled=true
  • 关闭时返回 403
  • provider 不存在时返回 400
  • 外部会话不存在或校验失败时返回 401
  • 成功时返回与 /api/v1/auth/me 相同的用户结构,并建立标准 Session

/api/v1/auth/direct/login 请求示例:

{
  "provider": "private-sso",
  "username": "alice",
  "password": "secret"
}

/api/v1/auth/direct/login 协议约束:

  • 开源版默认关闭,需显式开启 skillhub.auth.direct.enabled=true
  • 关闭时返回 403
  • provider 不存在时返回 400
  • 成功时返回与 /api/v1/auth/me 相同的用户结构,并建立标准 Session
  • /api/v1/auth/local/login 继续保留,作为现有本地账号入口

7.3 Authenticated API(需登录)

方法 路径 说明
POST /api/v1/skills/{namespace}/{slug}/star 收藏
DELETE /api/v1/skills/{namespace}/{slug}/star 取消收藏
POST /api/v1/skills/{namespace}/{slug}/rating 评分
GET /api/v1/skills/{skillId}/reviews/me 当前用户的评分与文字评价
PUT /api/v1/skills/{skillId}/reviews/me 新增或更新当前用户评价(score 1-5,reviewText 最长 2000)
DELETE /api/v1/skills/{skillId}/reviews/me 删除文字评价并保留星级评分
GET /api/v1/me/stars 我的收藏列表
GET /api/v1/me/skills 我发布的技能列表

草稿与审核提交(Phase 3 引入)

方法 路径 说明
POST /api/v1/skills/{namespace}/{slug}/versions/{version}/withdraw-review 撤回提审(PENDING_REVIEW → DRAFT,同时删除关联的 PENDING review_task)
POST /api/v1/reviews 提交指定 skillVersionId 进入审核队列

标签管理

方法 路径 说明
GET /api/v1/skills/{namespace}/{slug}/tags 列出标签
PUT /api/v1/skills/{namespace}/{slug}/tags/{tagName} 创建/移动自定义标签(latest 为系统保留标签,不可通过此接口操作)
DELETE /api/v1/skills/{namespace}/{slug}/tags/{tagName} 删除自定义标签(latest 不可删)

技能生命周期管理

方法 路径 说明
POST /api/v1/skills/{namespace}/{slug}/archive 归档技能(namespace ADMIN 或 owner)
POST /api/v1/skills/{namespace}/{slug}/unarchive 恢复归档(namespace ADMIN 或 owner)
DELETE /api/v1/skills/{namespace}/{slug}/versions/{version} 删除 DRAFT/REJECTED 版本
POST /api/v1/skills/{namespace}/{slug}/versions/{version}/yank 撤回已发布版本(namespace ADMIN 或 owner;可选 body {"reason"};API Token 需 skill:publish)
POST /api/v1/skills/{namespace}/{slug}/versions/{version}/rerelease 从已发布版本重新发出一个新版本(namespace ADMIN 或 owner)

当前代码中的 skill 生命周期读模型不再依赖 latestVersionStatus / viewingVersionStatus 一类拼装字段,而统一使用以下 projection:

  • headlineVersion:当前页面应展示的主版本
  • publishedVersion:当前最新可分发的已发布版本
  • ownerPreviewVersion:owner / namespace 管理者可见的 PENDING_REVIEW 预览版本
  • resolutionMode:PUBLISHED / OWNER_PREVIEW / NONE

其中:

  • 公开详情、公开安装、公开搜索一律只认 publishedVersion
  • owner 详情页在没有可展示发布版本时,才允许 headlineVersion 落到 ownerPreviewVersion
  • 推广到全局一律使用 publishedVersion.id
  • hidden 是独立治理覆盖层,不属于生命周期状态机
  • 常规版本详情接口只放行 PUBLISHED,以及 owner 对自己 PENDING_REVIEW 版本的预览;DRAFT / REJECTED / YANKED 不通过该接口暴露

发布成功响应中的 data 至少包含以下字段:

  • skillId
  • namespace
  • slug
  • version
  • status
  • fileCount
  • totalSize

发布状态约束:

  • 普通用户发布成功后,status 为 PENDING_REVIEW
  • 持有 SUPER_ADMIN 的用户通过 Web、/api/v1/publish、/api/v1/publish 发布时,status 为 PUBLISHED,且不要求其必须是目标 namespace 成员
  • 当前版本保持该审核策略,不再提供“全员直发”的运行模式
  • 撤回审核不会删除版本记录,而是 PENDING_REVIEW → DRAFT

7.4 Token API(需登录)

方法 路径 说明
POST /api/v1/tokens 创建 API Token
GET /api/v1/tokens 列出我的 Token
DELETE /api/v1/tokens/{id} 吊销 Token

7.5 CLI API(Bearer Token 认证,CLI 主流程由 Device Flow 获取凭证)

方法 路径 说明
GET /api/v1/whoami 当前 Bearer Token 对应的用户信息
POST /api/v1/publish 发布技能包(普通用户进入审核;SUPER_ADMIN 始终直发)
GET /api/v1/resolve/{namespace}/{slug} 解析版本
GET /api/v1/check/{namespace}/{slug}/{version} 本地哈希与远端比对

ClawHub CLI 协议兼容层

一期不仅提供 skillhub 自有 CLI API,还必须暴露一组兼容 ClawHub CLI 的 registry API。

  • 目标:让现有 ClawHub CLI 可通过配置 registry base URL 直接对接 skillhub
  • 范围:一期聚焦覆盖 ClawHub CLI 所依赖的核心接口:查询、版本解析、下载、发布、whoami
  • 要求:兼容层优先保持 ClawHub CLI 既有请求/响应语义;若内部领域模型不同,通过 adapter 层完成协议转换,而不是要求客户端适配 skillhub 私有协议
  • 要求:兼容层纳入 OpenAPI 或独立兼容协议文档,并作为正式对外契约维护
  • 要求:兼容层与 skillhub 自有 /api/v1/** 并存,二者共享同一套权限、审计、限流与领域服务
  • 非目标:前端页面不直接依赖兼容层;兼容层用于服务已有 ClawHub CLI 和相关自动化脚本

兼容层最少需要覆盖的能力类别:

  • Registry metadata:技能查询、技能详情、版本列表、标签/默认版本解析
  • Artifact resolution:按技能坐标或版本解析下载地址/下载流
  • Publish workflow:包上传与发布结果返回
  • Integrity check:版本存在性校验、摘要/哈希比对、whoami/token 上下文确认

如 ClawHub CLI 的现有协议与 skillhub 自有接口存在差异,文档以“兼容 ClawHub CLI 协议”为准,skillhub 内部 API 可继续保持当前风格。

7.6 Admin API(需对应平台角色)

Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN:

平台治理接口

方法 路径 说明
GET /api/v1/promotions 待审核提升申请列表(需 SKILL_ADMIN / SUPER_ADMIN;路由不在 /admin/* 下)
GET /api/v1/promotions/{id} 提升申请详情(提交人本人或 SKILL_ADMIN / SUPER_ADMIN 可读)
POST /api/v1/promotions/{id}/approve 通过提升申请(需 SKILL_ADMIN / SUPER_ADMIN)
POST /api/v1/promotions/{id}/reject 拒绝提升申请(需 SKILL_ADMIN / SUPER_ADMIN)
POST /api/v1/admin/skills/{id}/hide 隐藏技能(仅 SUPER_ADMIN)
POST /api/v1/admin/skills/{id}/unhide 恢复技能(仅 SUPER_ADMIN)
POST /api/v1/admin/skills/versions/{versionId}/yank 撤回已发布版本(SKILL_ADMIN / SUPER_ADMIN)
POST /api/v1/admin/skill-reviews/{reviewId}/hide 隐藏用户评价(SKILL_ADMIN / SUPER_ADMIN)
POST /api/v1/admin/skill-reviews/{reviewId}/restore 恢复用户评价(SKILL_ADMIN / SUPER_ADMIN)

用户治理(需 USER_ADMIN / SUPER_ADMIN)

方法 路径 说明
GET /api/v1/admin/users 用户列表
GET /api/v1/admin/users/{id} 用户详情
PUT /api/v1/admin/users/{id}/role 修改用户角色(USER_ADMIN 不可分配 SUPER_ADMIN,也不可修改已有 SUPER_ADMIN 的角色状态)
POST /api/v1/admin/users/{id}/approve 审批待准入用户
POST /api/v1/admin/users/{id}/disable 封禁用户
POST /api/v1/admin/users/{id}/enable 解封用户

审计(需 AUDITOR / SUPER_ADMIN)

方法 路径 说明
GET /api/v1/admin/audit-logs 审计日志查询

7.7 Namespace 管理 API(需命名空间 OWNER 或 ADMIN)

方法 路径 说明
POST /api/v1/namespaces 创建命名空间
PUT /api/v1/namespaces/{slug} 更新命名空间信息
GET /api/v1/namespaces/{slug}/members 成员列表
POST /api/v1/namespaces/{slug}/members 添加成员
PUT /api/v1/namespaces/{slug}/members/{userId}/role 修改成员角色
DELETE /api/v1/namespaces/{slug}/members/{userId} 移除成员
GET /api/v1/reviews?namespaceId={id} 该空间待审核列表
POST /api/v1/reviews/{id}/approve 空间管理员审核通过
POST /api/v1/reviews/{id}/reject 空间管理员审核拒绝
POST /api/v1/promotions 申请提升到全局

7.8 latest 语义说明

latest 自动跟随最新已发布版本,不可手动移动。

  • skill.latest_version_id:每次审核通过自动更新,始终指向最新 PUBLISHED 版本
  • yank 当前最新已发布版本时,需要同步重算 latest_version_id 指向下一个最新的 PUBLISHED 版本;若不存在则允许为 null
  • latest 标签:系统保留,只读,自动与 latest_version_id 同步
  • 自定义标签(如 beta、stable-2026q1):允许人工创建和移动,用于固定安装通道
场景 使用字段 说明
搜索索引内容 publishedVersion / latest_version_id 外部协议仍叫 latest,但内部语义必须等价于最新已发布版本
/download(不带版本号) publishedVersion / latest_version_id 下载最新已发布版本
CLI install @team/skill publishedVersion / latest_version_id 等同于 @latest
CLI install @team/skill@beta skill_tag 查询 自定义标签指向的版本

7.9 Resolve 接口说明

GET /api/v1/skills/{namespace}/{slug}/resolve 用于解析技能版本,支持以下 query param:

参数 类型 说明
version string 精确版本号(如 1.2.0)
tag string 标签名(如 beta、latest)
hash string fingerprint 哈希,用于判断本地版本是否与 registry 同步

解析优先级:

  1. version 和 tag 不可同时传,同时传返回 400 Bad Request
  2. 仅传 version:精确匹配版本号
  3. 仅传 tag:查询 skill_tag 表获取 target_version_id
  4. 仅传 hash:遍历已发布版本,比对 fingerprint
  5. 均不传:返回最新已发布版本;实现上可由 latest_version_id 或等价 published projection 解析

响应:

{
  "code": 0,
  "msg": "获取成功",
  "data": {
    "skillId": 456,
    "namespace": "team-name",
    "slug": "my-skill",
    "version": "1.2.0",
    "versionId": 123,
    "fingerprint": "sha256:abc123...",
    "downloadUrl": "/api/v1/skills/team-name/my-skill/versions/1.2.0/download"
  },
  "timestamp": "2026-03-12T06:00:00Z",
  "requestId": "req-123"
}

hash 匹配时额外返回 "matched": true,不匹配时返回最新版本信息 + "matched": false。

7.10 ClawHub CLI 兼容层 API

兼容层 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。

Well-known 发现

GET /.well-known/clawhub.json

响应:
{
  "apiBase": "/api/v1"
}

兼容层端点

方法 路径 说明
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/v1/whoami

{
  "handle": "username",
  "displayName": "User Name",
  "role": "user"
}

GET /api/v1/search?q={keyword}&page={page}&limit={limit}

{
  "results": [
    {
      "slug": "my-skill",
      "name": "My Skill",
      "description": "...",
      "author": { "handle": "username", "displayName": "User Name" },
      "version": "1.2.0",
      "downloadCount": 100,
      "starCount": 50,
      "createdAt": "2026-01-01T00:00:00Z",
      "updatedAt": "2026-03-01T00:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20
}

注意:兼容层返回的 slug 为 canonical slug 格式(全局空间直接返回 skill slug,团队空间返回 namespace--skill)。

GET /api/v1/resolve?slug={slug}&version={version}

{
  "slug": "my-skill",
  "version": "1.2.0",
  "downloadUrl": "/api/v1/download/my-skill/1.2.0"
}

GET /api/v1/download/{slug}/{version}

返回指定版本的 zip 文件流。默认版本解析由 resolve 接口负责。

POST /api/v1/publish

Content-Type: multipart/form-data
Parts:
  - file: zip 包

一期同步响应,返回发布结果:

{
  "slug": "my-skill",
  "version": "1.0.0",
  "status": "published"
}

兼容层适配说明

  • 兼容层是独立的 Controller 层,内部调用与 native API 相同的领域服务
  • 请求进入时将 canonical slug 转换为 (namespace_id, skill_slug) 坐标
  • 响应返回时将内部坐标转换为 canonical slug
  • 兼容层不暴露 namespace 概念,对 ClawHub CLI 透明
  • 发布时如果 canonical slug 包含 --,解析为团队空间发布;否则发布到全局空间
  • 兼容层的认证复用 skillhub Bearer Token 体系,ClawHub CLI 通过登录或配置 token 后即可使用

7.11 Rate Limiting

分两阶段实施:

Phase 1:Ingress 层基础限流

通过 Nginx Ingress limit-req 按 IP 全局限流,覆盖认证、搜索、下载等匿名可访问接口,防止基本的滥用和爬虫。

Phase 2:应用层精细限流

基于 Redis 滑动窗口,按用户/端点分类的精细限流。

端点类别 限流策略
搜索 API 已登录 60 次/分钟,匿名 20 次/分钟(按 IP)
下载 API 已登录 120 次/分钟,匿名 30 次/分钟(按 IP)
发布 API 10 次/小时(按用户)
认证 API 30 次/分钟(按 IP)

触发限流时返回 429 Too Many Requests + Retry-After Header。

7.12 API 设计原则

Native API(/api/v1/*)

  • 统一响应包裹:{ code, message, data, timestamp }
  • 分页格式:{ items, total, page, size }
  • 错误码体系:业务错误码 + HTTP 状态码配合
  • 版本策略:URL path 版本 /api/v1/
  • 幂等性:写操作通过 X-Request-Id + Redis 去重(TTL 24h)

Compatibility API(/api/v1/*)

  • 响应格式完全遵循 ClawHub 协议,不套统一响应包裹
  • 错误响应遵循 ClawHub 格式:{ error: string, message: string }
  • 分页格式遵循 ClawHub 格式:{ results, total, page, limit }

OpenAPI 文档分离

生成两份独立的 OpenAPI spec:

  • openapi-native.json:skillhub Native API,用于前端 SDK 生成
  • openapi-compat-clawhub.json:ClawHub 兼容层 API,用于兼容性测试和文档