* feat(skill): add UPLOADED status for PRIVATE skill lifecycle ## Summary - Add UPLOADED status for PRIVATE skills after security scan passes - PRIVATE skill owners can test before confirming publish or submitting for review - Rerelease now follows visibility rules (PRIVATE→UPLOADED, PUBLIC→PENDING_REVIEW) - Auto-withdraw changes status to UPLOADED (not DRAFT) to keep versions visible ## Changes - SkillVersionStatus: Add UPLOADED enum value - SkillPublishService: PRIVATE skills go to UPLOADED after scan - SecurityScanService: Visibility-based status transition after scan - SkillGovernanceService: Withdraw→UPLOADED, delete allows UPLOADED - SkillQueryService: Include UPLOADED in version list filters - SkillReviewSubmitService: New service for submit-review and confirm-publish - SkillLifecycleController: Add submit-review and confirm-publish endpoints - Frontend: Add buttons, dialogs, and hooks for new operations ## Workflow - PRIVATE: Publish → SCANNING → UPLOADED → confirm-publish → PUBLISHED - PUBLIC: Publish → SCANNING → PENDING_REVIEW → PUBLISHED * feat(review): add backward compatibility for DRAFT status Support both DRAFT (legacy) and UPLOADED (new flow) status in: - SkillReviewSubmitService.submitForReview - SkillReviewSubmitService.confirmPublish - ReviewService.submitReview (both overloads) This ensures existing data with DRAFT status continues to work with the new visibility-based workflow introduced in OSS-02.
19 KiB
OSS-01 Core 契约审计与冻结
1. 审计结论
SkillHub 开源项目已具备 AstronClaw 主链路所需的绝大部分 Core 能力。现有接口覆盖了 skill 唯一标识查询、版本元数据查询、创建(发布)和删除。无需在开源 Core 中新增 AstronClaw 专属接口;对 AstronClaw 而言,查询类和主链路类能力都应统一由 SaaS 层 AstronClaw Adapter 封装后对外提供,而不是直接绑定开源 Core 的接口形态。
2. Core 接口清单
以下接口构成 Core 基线能力,供 SaaS 层统一封装后对 AstronClaw 提供;这些接口本身不应被视为 AstronClaw 的长期直接契约。
2.1 skill 唯一标识与详情查询
| 接口 | 路径 | 说明 |
|---|---|---|
| skill 详情 | GET /api/v1/skills/{namespace}/{slug} |
返回 SkillDetailResponse,包含完整 identity 和状态 |
| 版本解析 | GET /api/v1/skills/{namespace}/{slug}/resolve?version=&tag=&hash= |
返回 ResolveVersionResponse,解析人类可读版本选择器到精确版本 |
2.2 指定版本安装元数据查询
| 接口 | 路径 | 说明 |
|---|---|---|
| 版本详情 | GET /api/v1/skills/{namespace}/{slug}/versions/{version} |
返回 SkillVersionDetailResponse,含 metadata 和 manifest |
| 版本文件列表 | GET /api/v1/skills/{namespace}/{slug}/versions/{version}/files |
返回 List<SkillFileResponse> |
| 版本下载 | GET /api/v1/skills/{namespace}/{slug}/versions/{version}/download |
下载指定版本 bundle |
| 版本列表 | GET /api/v1/skills/{namespace}/{slug}/versions?page=&size= |
分页返回版本列表 |
2.3 创建(发布)个人 skill
| 接口 | 路径 | 说明 |
|---|---|---|
| 发布 skill | POST /api/v1/skills/{namespace}/publish |
上传包并发布,返回 PublishResponse |
2.4 删除个人 skill
| 接口 | 路径 | 说明 |
|---|---|---|
| 硬删除(by ID) | DELETE /api/v1/skills/id/{skillId} |
需 SUPER_ADMIN 权限 |
| 硬删除(by 坐标) | DELETE /api/v1/skills/{namespace}/{slug} |
需 SUPER_ADMIN 权限 |
| 归档 | POST /api/v1/skills/{namespace}/{slug}/archive |
owner 或 namespace admin 可操作 |
| 取消归档 | POST /api/v1/skills/{namespace}/{slug}/unarchive |
恢复为 ACTIVE |
2.5 版本生命周期
| 接口 | 路径 | 说明 |
|---|---|---|
| 删除版本 | DELETE /api/v1/skills/{namespace}/{slug}/versions/{version} |
仅 DRAFT/REJECTED/SCAN_FAILED 可删 |
| 撤回审核 | POST /api/v1/skills/{namespace}/{slug}/versions/{version}/withdraw-review |
PENDING_REVIEW → DRAFT |
| 重新发布 | POST /api/v1/skills/{namespace}/{slug}/versions/{version}/rerelease |
重新发布版本 |
2.6 ClawHub 兼容接口(已有)
| 接口 | 路径 | 说明 |
|---|---|---|
| 解析 skill | GET /api/v1/resolve?slug=&version= |
ClawHub 协议兼容 |
| 解析 skill(路径) | GET /api/v1/resolve/{canonicalSlug}?version= |
ClawHub 协议兼容 |
| 下载 | GET /api/v1/download/{canonicalSlug}?version= |
302 重定向到下载地址 |
| 删除 skill | DELETE /api/v1/skills/{canonicalSlug} |
owner 可操作 |
| 取消删除 | POST /api/v1/skills/{canonicalSlug}/undelete |
owner 可操作 |
| 发布 skill | POST /api/v1/skills |
ClawHub 协议兼容 |
| 发布到 namespace | POST /api/v1/publish |
ClawHub 协议兼容 |
3. 字段语义冻结表
3.1 Skill Identity 字段
| 字段 | 类型 | 含义 | 稳定性 | 说明 |
|---|---|---|---|---|
skill.id |
Long | skill 全局唯一主键 | 不可变 | 自增,创建后永不改变,可作为外部映射主键 |
namespace (slug) |
String(64) | skill 所属命名空间标识 | 不可变 | 全局唯一,创建后不可改名 |
skill.slug |
String(100) | skill 在 namespace 内的唯一标识 | 不可变 | 创建后不可改名,namespace + slug 构成业务坐标 |
skill.displayName |
String(200) | skill 展示名称 | 可变 | 仅用于展示,不可作为映射依据 |
skill.ownerId |
String | skill 创建者 ID | 不可变 | 创建时绑定,不可转移 |
skill.summary |
String(TEXT) | skill 简介 | 可变 | 展示用 |
skill.visibility |
Enum | 可见性 | 可变 | PUBLIC / NAMESPACE_ONLY / PRIVATE |
skill.status |
Enum | skill 状态 | 可变 | ACTIVE / HIDDEN / ARCHIVED |
skill.hidden |
boolean | 是否被管理员隐藏 | 可变 | 与 status 独立的隐藏标记 |
skill.latestVersionId |
Long | 最新版本指针 | 可变 | 指向当前最新已发布版本,yank/删除后自动回退 |
skill.downloadCount |
Long | 下载次数 | 可变 | 累计值 |
skill.starCount |
Integer | 收藏数 | 可变 | 累计值 |
3.2 SkillVersion 字段
| 字段 | 类型 | 含义 | 稳定性 | 说明 |
|---|---|---|---|---|
version.id |
Long | 版本全局唯一主键 | 不可变 | 自增 |
version.skillId |
Long | 所属 skill ID | 不可变 | 外键 |
version.version |
String(64) | 版本号 | 不可变 | 如 1.0.0,创建后不可改 |
version.status |
Enum | 版本状态 | 可变 | 见状态语义表 |
version.bundleReady |
boolean | bundle 是否可用 | 可变 | true 表示 bundle 已构建完成,可下载安装 |
version.downloadReady |
boolean | 是否允许下载 | 可变 | yank 后设为 false |
version.publishedAt |
Instant | 发布时间 | 一次写入 | 首次发布时设置 |
version.parsedMetadataJson |
JSONB | 解析后的元数据 | 一次写入 | 包含 package_name 等运行时信息 |
version.manifestJson |
JSONB | manifest 原始内容 | 一次写入 | skill 包的 manifest |
version.changelog |
String(TEXT) | 变更日志 | 可变 | 展示用 |
version.fileCount |
Integer | 文件数量 | 一次写入 | 发布时确定 |
version.totalSize |
Long | 总大小(字节) | 一次写入 | 发布时确定 |
version.yankedAt |
Instant | yank 时间 | 一次写入 | yank 时设置 |
version.yankReason |
String(TEXT) | yank 原因 | 一次写入 | yank 时设置 |
3.3 关键字段含义冻结
| 字段 | 冻结定义 |
|---|---|
skill_id |
skill.id,Long 类型自增主键,全局唯一,创建后不可变。AstronClaw 应以此作为 external_skill_mapping 的外部主键 |
namespace |
namespace.slug,String(64),全局唯一,不可改名。与 slug 组合构成业务坐标 |
slug |
skill.slug,String(100),namespace 内唯一,不可改名。namespace/slug 是人类可读的稳定坐标 |
version |
skill_version.version,String(64),同一 skill 内唯一,不可改。如 1.0.0 |
bundle_url |
通过 GET /{namespace}/{slug}/versions/{version}/download 获取,或通过 resolve 接口的 downloadUrl 字段获取。不是数据库字段,而是动态生成的下载地址 |
bundle_ready |
skill_version.bundleReady,boolean。true 表示 bundle 已构建完成可安装。AstronClaw 安装前必须校验此字段 |
package_name |
存储在 skill_version.parsedMetadataJson 中,从 skill 包的 manifest 解析而来。同一 skill 跨版本应保持稳定。AstronClaw 用于运行时安装/卸载标识 |
3.4 Namespace 字段
| 字段 | 类型 | 含义 | 稳定性 |
|---|---|---|---|
namespace.id |
Long | 命名空间主键 | 不可变 |
namespace.slug |
String(64) | 命名空间标识 | 不可变,全局唯一 |
namespace.displayName |
String(128) | 展示名称 | 可变 |
namespace.type |
Enum | 类型 | 不可变,GLOBAL / TEAM |
namespace.status |
Enum | 状态 | 可变,ACTIVE / FROZEN / ARCHIVED |
4. 状态语义冻结表
4.1 Skill 状态(SkillStatus)
| 状态 | 市场可见 | 可新装 | 已装是否保留 | 可被 owner 操作 | 说明 |
|---|---|---|---|---|---|
ACTIVE |
是(受 visibility 控制) | 是(需有 PUBLISHED 版本) | 是 | 是 | 正常状态 |
HIDDEN |
否 | 否 | 是 | 受限 | 管理员隐藏,独立于 status 的 hidden 标记 |
ARCHIVED |
否 | 否 | 是 | 可取消归档 | owner 或 namespace admin 归档 |
4.2 版本状态(SkillVersionStatus)
| 状态 | 是否允许安装 | 是否允许下载 | 市场可见 | 可转换到 | 说明 |
|---|---|---|---|---|---|
DRAFT |
否 | 否 | 否 | SCANNING, 可删除 | 初始状态,编辑中 |
SCANNING |
否 | 否 | 否 | SCAN_FAILED, PENDING_REVIEW, PUBLISHED | 安全扫描中 |
SCAN_FAILED |
否 | 否 | 否 | 可删除 | 安全扫描失败 |
PENDING_REVIEW |
否 | 否 | 否 | PUBLISHED, REJECTED, → DRAFT(撤回) | 等待审核 |
PUBLISHED |
是 | 是 | 是 | YANKED | 已发布,可安装 |
REJECTED |
否 | 否 | 否 | 可删除 | 审核拒绝 |
YANKED |
否 | 否 | 否(或弱可见) | 不可逆 | 已撤回,已装不受影响 |
4.3 可见性(SkillVisibility)
| 可见性 | 市场列表可见 | 谁可查看 | 谁可安装 |
|---|---|---|---|
PUBLIC |
是 | 所有人 | 所有人(需 PUBLISHED + bundleReady) |
NAMESPACE_ONLY |
否 | namespace 成员 | namespace 成员 |
PRIVATE |
否 | 仅 owner | 仅 owner |
4.4 删除语义
| 操作 | 类型 | 可逆 | 数据影响 | 已装实例影响 |
|---|---|---|---|---|
| 硬删除 skill | 永久删除 | 否 | 删除所有记录、文件、存储对象,slug 可复用 | 不影响,AstronClaw 已装快照独立 |
| 归档 skill | 状态变更 | 是 | 无数据删除,status → ARCHIVED | 不影响 |
| 隐藏 skill | 标记变更 | 是 | 无数据删除,hidden → true | 不影响 |
| 删除版本 | 永久删除 | 否 | 仅删除 DRAFT/REJECTED/SCAN_FAILED 版本 | 不影响(这些版本未被安装) |
| Yank 版本 | 状态变更 | 否 | status → YANKED,downloadReady → false | 不影响已装实例 |
4.5 AstronClaw 安装判断规则
AstronClaw 判断一个 skill 版本是否可安装,需同时满足:
skill.status == ACTIVE
AND skill.hidden == false
AND skill.visibility 允许当前用户访问
AND version.status == PUBLISHED
AND version.bundleReady == true
已安装实例不受后续状态变更影响。即使 skill 被删除/归档/隐藏,或版本被 yank,AstronClaw 本地安装快照仍可正常使用和卸载。
5. 错误语义表
5.1 统一响应结构
{
"code": 0,
"msg": "操作成功",
"data": { ... },
"timestamp": "2026-04-10T08:00:00Z",
"requestId": "req-xxx"
}
code = 0表示成功code > 0表示错误,值为 HTTP 状态码
5.2 错误码映射
| HTTP 状态码 | 场景 | 异常类型 | 说明 |
|---|---|---|---|
| 400 | 参数非法 | BadRequestException / DomainBadRequestException |
请求参数校验失败 |
| 401 | 未认证 | UnauthorizedException / AuthFlowException |
未登录或 token 过期 |
| 403 | 无权限 | ForbiddenException / DomainForbiddenException |
无操作权限 |
| 404 | 未找到 | DomainNotFoundException |
skill/version/namespace 不存在 |
| 408 | 请求超时 | AsyncRequestTimeoutException |
异步请求超时 |
| 503 | 存储不可用 | StorageAccessException |
对象存储访问失败 |
| 500 | 服务异常 | Exception |
未预期的内部错误 |
5.3 Core 主链路关键错误场景
| 场景 | HTTP 状态码 | msg 示例 | AstronClaw 处理建议 |
|---|---|---|---|
| skill 不存在 | 404 | error.skill.notFound |
映射失败,提示用户 |
| 版本不存在 | 404 | error.skill.notFound |
安装/升级失败,提示用户 |
| 版本不可安装(非 PUBLISHED) | 400 | error.badRequest |
拒绝安装,提示版本状态 |
| bundle 未就绪 | 400 | error.badRequest |
拒绝安装,提示稍后重试 |
| 无权访问(PRIVATE skill) | 403 | error.forbidden |
提示无权限 |
| namespace 不存在 | 404 | error.namespace.notFound |
映射失败 |
| 存储服务不可用 | 503 | error.storage.unavailable |
降级处理,已装 skill 不受影响 |
| 删除不允许(非 owner) | 403 | error.forbidden |
提示无权限 |
6. Core vs SaaS Adapter 能力分界
6.1 Core 已满足的能力
说明:
下表表示“开源 Core 已具备、可供 SaaS 封装”的能力,并不表示 AstronClaw 应直接调用这些开源接口。
| PRD 需求 | Core 接口 | 满足程度 | 备注 |
|---|---|---|---|
| skill 唯一标识查询 | GET /{namespace}/{slug} |
完全满足 | 返回 id、namespace、slug |
| 指定版本安装元数据 | GET /{namespace}/{slug}/versions/{version} |
基本满足 | 返回 status、metadata;package_name 在 parsedMetadataJson 中 |
| 版本解析 | GET /{namespace}/{slug}/resolve |
完全满足 | 支持 version/tag/hash 解析 |
| bundle 下载 | GET /{namespace}/{slug}/versions/{version}/download |
完全满足 | 直接下载 |
| 创建(发布)个人 skill | POST /{namespace}/publish |
完全满足 | 返回 skillId、namespace、slug、version、status |
| 删除个人 skill | DELETE /{namespace}/{slug} (ClawHub 兼容) |
完全满足 | owner 可操作 |
| 归档 skill | POST /{namespace}/{slug}/archive |
完全满足 | 可逆操作 |
| 版本状态查询 | GET /{namespace}/{slug} 中的 headlineVersion/publishedVersion |
完全满足 | 包含版本状态 |
| labels 数据 | GET /{namespace}/{slug} 中的 labels 字段 |
完全满足 | 返回 List<SkillLabelDto> |
6.2 需要 SaaS Adapter 新增的能力
| PRD 需求 | 原因 | Adapter 建议 |
|---|---|---|
| 市场列表查询(搜索/过滤/排序) | Core 不提供面向页面的聚合列表 | GET /api/v1/astronclaw/adapter/skills/market |
| 市场详情(AstronClaw DTO) | Core 返回的 DTO 包含 Core 内部字段,需适配 | GET /api/v1/astronclaw/adapter/skills/{id} |
| owner 维度"我创建的"查询 | Core 的 /me/skills 返回 Core DTO,需适配 |
GET /api/v1/astronclaw/adapter/skills/mine |
is_installed 补全 |
安装关系在 AstronClaw 侧 | AstronClaw 本地补全,不在 Adapter |
package_name 顶层字段 |
当前在 parsedMetadataJson 内,需提取 |
Adapter 解析 JSON 后平铺返回 |
bundle_url 直接返回 |
当前需通过 download 接口获取 | Adapter 可直接返回预签名 URL |
统一 can_install 判断 |
需组合 status + visibility + bundleReady | Adapter 计算后返回布尔值 |
统一 can_delete 判断 |
需组合 owner + status | Adapter 计算后返回布尔值 |
6.3 分界原则
Core 负责:skill 生命周期真相(identity、version、status、artifact)
Adapter 负责:面向 AstronClaw 的 DTO 适配(字段平铺、状态聚合、权限预判断)
补充原则:
- 即使开源
Core已经具备某项主链路能力,AstronClaw仍应统一通过 SaaS Adapter 消费。 - 该原则同时适用于唯一标识查询、版本元数据、创建个人 skill、删除个人 skill。
- 开源文档中的接口清单用于说明
Core能力边界,不应被解读为 AstronClaw 的直接对接建议。
7. 成功 / 失败 / 边界样例
7.1 查询 skill identity — 成功
GET /api/v1/skills/my-namespace/my-skill
{
"code": 0,
"data": {
"id": 42,
"slug": "my-skill",
"displayName": "My Skill",
"ownerId": "user-123",
"status": "ACTIVE",
"visibility": "PUBLIC",
"namespace": "my-namespace",
"labels": [{"slug": "nlp", "type": "CATEGORY", "displayName": "NLP"}],
"headlineVersion": {"id": 100, "version": "1.2.0", "status": "PUBLISHED"},
"publishedVersion": {"id": 100, "version": "1.2.0", "status": "PUBLISHED"}
}
}
AstronClaw 映射关键字段:id=42,namespace=my-namespace,slug=my-skill。
7.2 查询 skill identity — 不存在
GET /api/v1/skills/my-namespace/nonexistent
{
"code": 404,
"msg": "Skill not found",
"data": null
}
7.3 查询指定版本元数据 — 成功
GET /api/v1/skills/my-namespace/my-skill/versions/1.2.0
{
"code": 0,
"data": {
"id": 100,
"version": "1.2.0",
"status": "PUBLISHED",
"changelog": "Bug fixes",
"fileCount": 3,
"totalSize": 102400,
"publishedAt": "2026-04-01T10:00:00Z",
"parsedMetadataJson": "{\"name\":\"my-skill\",\"package_name\":\"my_namespace__my_skill\",\"version\":\"1.2.0\"}",
"manifestJson": "{...}"
}
}
package_name 从 parsedMetadataJson 中提取。
7.4 查询已 YANKED 版本
GET /api/v1/skills/my-namespace/my-skill/versions/1.0.0
{
"code": 0,
"data": {
"id": 98,
"version": "1.0.0",
"status": "YANKED",
"publishedAt": "2026-03-01T10:00:00Z"
}
}
AstronClaw 判断 status != PUBLISHED,拒绝新安装。已装实例不受影响。
7.5 发布(创建)个人 skill — 成功
POST /api/v1/skills/my-namespace/publish
Content-Type: multipart/form-data
file: <skill-package.tar.gz>
visibility: PRIVATE
{
"code": 0,
"data": {
"skillId": 43,
"namespace": "my-namespace",
"slug": "new-skill",
"version": "0.1.0",
"status": "DRAFT",
"fileCount": 2,
"totalSize": 51200
}
}
7.6 删除个人 skill — 成功
DELETE /api/v1/skills/my-namespace/my-skill
{
"code": 0,
"data": {
"ok": true
}
}
7.7 删除个人 skill — 无权限
DELETE /api/v1/skills/other-namespace/other-skill
{
"code": 403,
"msg": "Forbidden",
"data": null
}
7.8 边界:skill 已归档后查询
GET /api/v1/skills/my-namespace/archived-skill
{
"code": 0,
"data": {
"id": 44,
"slug": "archived-skill",
"status": "ARCHIVED",
"visibility": "PUBLIC"
}
}
skill 仍可查询,但 AstronClaw 应根据 status=ARCHIVED 判断不可新装。
8. 遗留问题与建议
8.1 package_name 提取
当前 package_name 嵌套在 parsedMetadataJson JSONB 字段中,不是顶层字段。
建议:SaaS Adapter 在返回 AstronClaw DTO 时,解析 JSON 并将 package_name 提取为顶层字段。Core 不需要改动。
8.2 bundle_url 获取方式
当前没有直接返回 bundle_url 的字段,需通过 download 接口获取。ResolveVersionResponse 中有 downloadUrl 字段。
建议:SaaS Adapter 可通过 resolve 接口获取 downloadUrl,或直接生成预签名 URL 返回给 AstronClaw。
8.3 删除接口权限
当前 DELETE /api/v1/skills/{namespace}/{slug}(portal 路径)需要 SUPER_ADMIN 权限。ClawHub 兼容接口 DELETE /api/v1/skills/{canonicalSlug} 允许 owner 操作。
建议:SaaS Adapter 应统一封装 owner 可操作的删除接口,对 AstronClaw 暴露稳定契约;AstronClaw 不直接依赖开源删除接口路径。
8.4 hidden 与 status 的关系
当前 hidden 是独立于 status 的布尔标记(管理员操作),而 HIDDEN 是 SkillStatus 枚举值之一但实际代码中 skill 的 status 枚举包含 ACTIVE、HIDDEN、ARCHIVED。
建议:SaaS Adapter 统一为 AstronClaw 提供一个 is_visible 聚合字段,屏蔽内部 hidden 标记与 status 的复杂关系。