skillhub/docs/oss-01-core-contract-freeze.md
XiaoSeS 532d0450aa
feat(skill): add UPLOADED status for PRIVATE skill lifecycle (#290)
* 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.
2026-04-13 11:39:45 +08:00

460 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 → YANKEDdownloadReady → 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 被删除/归档/隐藏,或版本被 yankAstronClaw 本地安装快照仍可正常使用和卸载。
## 5. 错误语义表
### 5.1 统一响应结构
```json
{
"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 适配(字段平铺、状态聚合、权限预判断)
```
补充原则:
1. 即使开源 `Core` 已经具备某项主链路能力,`AstronClaw` 仍应统一通过 SaaS Adapter 消费。
2. 该原则同时适用于唯一标识查询、版本元数据、创建个人 skill、删除个人 skill。
3. 开源文档中的接口清单用于说明 `Core` 能力边界,不应被解读为 AstronClaw 的直接对接建议。
---
## 7. 成功 / 失败 / 边界样例
### 7.1 查询 skill identity — 成功
```
GET /api/v1/skills/my-namespace/my-skill
```
```json
{
"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
```
```json
{
"code": 404,
"msg": "Skill not found",
"data": null
}
```
### 7.3 查询指定版本元数据 — 成功
```
GET /api/v1/skills/my-namespace/my-skill/versions/1.2.0
```
```json
{
"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
```
```json
{
"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
```
```json
{
"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
```
```json
{
"code": 0,
"data": {
"ok": true
}
}
```
### 7.7 删除个人 skill — 无权限
```
DELETE /api/v1/skills/other-namespace/other-skill
```
```json
{
"code": 403,
"msg": "Forbidden",
"data": null
}
```
### 7.8 边界skill 已归档后查询
```
GET /api/v1/skills/my-namespace/archived-skill
```
```json
{
"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 的复杂关系。