skillhub/docs/oss-02-core-semantic-rules.md
xiose a1e4904d97 fix(i18n): update version delete error message to include UPLOADED and SCAN_FAILED
The error message for unsupported version deletion still referenced only
DRAFT/REJECTED. Updated both EN and ZH messages to reflect the actual
deletable statuses: DRAFT, UPLOADED, REJECTED, SCAN_FAILED.

Also updated OSS-02 design doc to mark all blocking items as completed.
2026-04-14 15:49:25 +08:00

663 lines
22 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-02 Core 语义规则收口
## 1. 文档目标
本文档固化 SkillHub Core 的运行时语义规则,确保开源版与 SaaS 版对删除、YANKED、同名冲突、package_name 等规则口径一致,避免 AstronClaw 接入后出现状态漂移。本文定义的是可由 SaaS 统一封装并对 AstronClaw 提供的 `Core` 规则基线,不表示 AstronClaw 直接对接这些开源接口。
---
## 2. 变更概要
### 2.1 新增功能
| 功能 | 说明 |
|------|------|
| UPLOADED 状态 | 新增版本状态,表示"已上传,未提交审核" |
| PRIVATE skill 自动发布 | PRIVATE skill 发布后进入 UPLOADED 状态,不自动进入审核 |
| 提交审核接口 | 新增 `POST /{namespace}/{slug}/submit-review`,允许 UPLOADED 状态的版本提交审核 |
| 撤回审核后进入 UPLOADED | 撤回审核后版本状态变为 UPLOADED而不是 DRAFT |
### 2.2 状态机变更
**变更前**
```
DRAFT → SCANNING → PENDING_REVIEW → PUBLISHED
↓ ↓
REJECTED YANKED
```
**变更后**
```
DRAFT → SCANNING → UPLOADED → PENDING_REVIEW → PUBLISHED
↓ ↓ ↓ ↓
SCAN_FAILED (可删除) REJECTED YANKED
↓ ↓
(可删除) (可删除)
```
### 2.3 权限模型变更
**核心原则**:权限只和 status 相关visibility 只影响状态流转。
---
## 3. 版本状态定义
### 3.1 状态枚举
```java
public enum SkillVersionStatus {
DRAFT, // 草稿,编辑中
SCANNING, // 安全扫描中
SCAN_FAILED, // 扫描失败
UPLOADED, // 已上传,未提交审核(新增)
PENDING_REVIEW, // 等待审核
PUBLISHED, // 已发布
REJECTED, // 审核拒绝
YANKED // 已撤回
}
```
### 3.2 状态语义
| 状态 | 含义 | 文件状态 | 可下载 | 可编辑 | 有检测报告 |
|------|------|---------|-------|-------|----------|
| DRAFT | 草稿,编辑中 | 可能不完整 | 否 | 是 | 否 |
| SCANNING | 安全扫描中 | 完整 | 否 | 否 | 否 |
| SCAN_FAILED | 扫描失败 | 完整 | 否 | 是 | 是(失败) |
| UPLOADED | 已上传,扫描通过 | 完整 | owner | 否 | 是 |
| PENDING_REVIEW | 审核中 | 完整 | owner | 否 | 是 |
| PUBLISHED | 已发布 | 完整 | 看 visibility | 否 | 是 |
| REJECTED | 审核拒绝 | 完整 | 否 | 是 | 是 |
| YANKED | 已撤回 | 完整 | 否 | 否 | 是 |
---
## 4. 发布流程设计
### 4.1 发布路径
| visibility | 发布后初始状态 | 是否创建审核任务 |
|------------|--------------|----------------|
| PRIVATE | UPLOADED | 否 |
| NAMESPACE_ONLY | PENDING_REVIEW | 是 |
| PUBLIC | PENDING_REVIEW | 是 |
### 4.2 PRIVATE skill 完整生命周期
```
用户发布 PRIVATE skill
状态SCANNING安全扫描中
扫描通过
状态UPLOADED
visibilityPRIVATE
owner 可下载/安装/测试
市场不可见
管理员可见(用于审计)
已有检测报告
owner 测试满意确认发布confirm-publish
状态PUBLISHED
visibilityPRIVATE正式私有版本
owner 可下载/安装
市场不可见
用户想公开,提交审核
状态PENDING_REVIEW
requestedVisibilityPUBLIC
owner 仍可下载/测试
审核通过
状态PUBLISHED
visibilityPUBLIC不再是 PRIVATE
市场可见,所有人可下载
```
### 4.3 PUBLIC/NAMESPACE_ONLY skill 生命周期
```
用户发布 PUBLIC/NAMESPACE_ONLY skill
状态PENDING_REVIEW
owner 可下载/测试
审核通过
状态PUBLISHED
visibilityPUBLIC 或 NAMESPACE_ONLY
市场可见(受 visibility 控制)
```
---
## 5. 权限矩阵
### 5.1 status 决定下载权限
| status | 市场可见 | 可下载 |
|--------|---------|-------|
| DRAFT | 否 | 否 |
| SCANNING | 否 | 否 |
| SCAN_FAILED | 否 | 否 |
| UPLOADED | 否 | owner |
| PENDING_REVIEW | 否 | owner |
| PUBLISHED | 看 visibility | 看 visibility |
| REJECTED | 否 | 否 |
| YANKED | 否 | 否 |
### 5.2 PUBLISHED 状态下visibility 决定可见性
| visibility | 市场可见 | 可下载 |
|------------|---------|-------|
| PUBLIC | 是 | 所有人 |
| NAMESPACE_ONLY | 命名空间内 | 命名空间成员 |
| PRIVATE | 否 | owner |
### 5.3 AstronClaw 安装判断规则
```
可安装 =
skill.status == ACTIVE
AND skill.hidden == false
AND 存在至少一个可下载版本
AND 该版本 bundleReady == true
可下载版本判断:
- UPLOADED/PENDING_REVIEW仅 owner
- PUBLISHED按 visibility 规则
```
---
## 6. 状态流转详细设计
### 6.1 状态转换表
| 当前状态 | 操作 | 目标状态 | 说明 |
|---------|------|---------|------|
| DRAFT | 上传包 | SCANNING | 开始安全扫描 |
| SCANNING | 扫描通过 | UPLOADED 或 PENDING_REVIEW | 看 visibility |
| SCANNING | 扫描失败 | SCAN_FAILED | - |
| SCAN_FAILED | 重新上传 | SCANNING | - |
| UPLOADED | 提交审核 | PENDING_REVIEW | 新增操作 |
| UPLOADED | 确认发布 | PUBLISHED | PRIVATE skill 正式发布,不触发新扫描 |
| UPLOADED | 重新上传 | SCANNING | 允许重新上传 |
| UPLOADED | 删除 | (删除) | 允许删除,未正式发布 |
| PENDING_REVIEW | 审核通过 | PUBLISHED | - |
| PENDING_REVIEW | 审核拒绝 | REJECTED | - |
| PENDING_REVIEW | 撤回审核 | UPLOADED | 变更:原为 DRAFT |
| PUBLISHED | Yank | YANKED | - |
| REJECTED | 重新上传 | SCANNING | - |
### 6.2 状态机图
```
┌─────────────────────────────────────────┐
│ 上传包 │
└─────────────────────────────────────────┘
┌───────────────┐
│ SCANNING │
└───────────────┘
/ \
扫描通过 / \ 扫描失败
/ \
┌────────────────────────┐ ┌───────────────┐
│ visibility=PRIVATE │ │ SCAN_FAILED │
│ → UPLOADED │ └───────────────┘
│ visibility=PUBLIC/ │ │
│ NAMESPACE_ONLY │ │ 重新上传
│ → PENDING_REVIEW │ ↓
└────────────────────────┘ ┌───────────────┐
│ │ SCANNING │
↓ └───────────────┘
┌────────────────────────┐
│ UPLOADED │◄────────────────────────┐
│ (PRIVATE skill 专属) │ │
│ 已有检测报告 │ │
└────────────────────────┘ │
/ \ │
确认发布 / \ 提交审核 │
(不触发新扫描) / \ │
/ \ │
↓ ↓ │
┌───────────────────┐ ┌───────────────────┐ │
│ PUBLISHED │ │ PENDING_REVIEW │ │
│ visibility=PRIVATE│ └───────────────────┘ │
└───────────────────┘ │ │
│ │ │
│ 提交审核 │ 审核通过 │
↓ ↓ │
┌───────────────────┐ ┌───────────────────┐ │
│ PENDING_REVIEW │ │ PUBLISHED │ │
└───────────────────┘ │ visibility=PUBLIC │ │
│ │ 或 NAMESPACE_ONLY │ │
│ └───────────────────┘ │
│ 撤回审核 │ │
└──────────────────────┘ │
(进入 UPLOADED) │
┌───────────────────┐ │
│ REJECTED │────────────────────────────────────────┘
└───────────────────┘ 重新上传
│ 删除
(删除)
```
---
## 7. 新增接口设计
说明:
以下接口属于开源 `Core` 为 SaaS 提供的基础状态机能力。对 `AstronClaw` 而言,后续仍应统一通过 `SkillHub SaaS``AstronClaw Adapter` 消费这些能力,而不是直接绑定这些开源接口路径。
### 7.1 提交审核接口
**接口**`POST /api/v1/skills/{namespace}/{slug}/submit-review`
**请求参数**
```json
{
"version": "1.0.0",
"targetVisibility": "PUBLIC"
}
```
**前置条件**
- 版本状态为 UPLOADED
- 操作者为 skill owner 或 namespace ADMIN/OWNER
**执行效果**
- 版本状态 → PENDING_REVIEW
- `requestedVisibility` 设为目标可见性
- 创建审核任务
**响应**
```json
{
"code": 0,
"data": {
"versionId": 100,
"status": "PENDING_REVIEW",
"requestedVisibility": "PUBLIC"
}
}
```
### 7.2 确认发布接口PRIVATE skill
**接口**`POST /api/v1/skills/{namespace}/{slug}/confirm-publish`
**请求参数**
```json
{
"version": "1.0.0"
}
```
**前置条件**
- 版本状态为 UPLOADED
- skill.visibility = PRIVATE
- 操作者为 skill owner
**执行效果**
- 版本状态 → PUBLISHED
- visibility 保持 PRIVATE
- **不触发新的扫描**,复用 UPLOADED 时的扫描结果
- 未来可扩展:加入"发布扫描"功能
**响应**
```json
{
"code": 0,
"data": {
"skillId": 42,
"versionId": 100,
"status": "PUBLISHED",
"visibility": "PRIVATE"
}
}
```
---
## 8. 删除 / 隐藏 / 归档 / YANKED 语义规则
### 8.1 操作语义总表
| 操作 | 触发方式 | 可逆 | 市场可见 | 可新装 | 已装保留 | 可卸载 | slug 可复用 |
|------|---------|------|---------|-------|---------|-------|-----------|
| **硬删除 skill** | owner 或 SUPER_ADMIN | 否 | 否 | 否 | 是 | 是 | 是 |
| **归档 skill** | owner / namespace admin | 是 | 否 | 否 | 是 | 是 | 否 |
| **隐藏 skill** | 管理员 | 是 | 否 | 否 | 是 | 是 | 否 |
| **Yank 版本** | owner / namespace admin | 否 | 否 | 否 | 是 | 是 | N/A |
### 8.2 Yank 版本
**定义**YANK 是"撤回已发布版本"的操作,用于将一个已发布的版本从可用状态移除。
**触发条件**
- owner 或 namespace ADMIN/OWNER 对 PUBLISHED 状态的版本执行 yank
**执行效果**
- `version.status``YANKED`(不可逆,无 un-yank 操作)
- `version.downloadReady``false`
- 记录 `yankedAt``yankedBy``yankReason`
- 如果该版本是 `skill.latestVersionId` 指向的版本:
- 自动回退到上一个 PUBLISHED 版本
- 如果没有其他 PUBLISHED 版本,`latestVersionId``null`
**对 AstronClaw 的影响**
- 已安装实例不受影响
- 无法新装该版本
- 升级场景:目标版本被 yank → 升级失败
对接原则:
- 上述语义应由 SaaS Adapter 原样继承并稳定对外提供
- AstronClaw 通过 Adapter 感知这些状态,不直接绑定开源返回形态
**补救方式**
- 不能 un-yank
- 只能发布新版本rerelease 或重新上传)
---
## 9. 同名冲突规则
### 9.1 唯一性约束
数据库约束:`UNIQUE(namespace_id, slug, owner_id)`
含义:
- 同一 namespace 下,不同 owner 可以有相同 slug
- 同一 namespace 下,同一 owner 只能有一个相同 slug 的 skill
### 9.2 冲突规则设计原则
**核心原则**:只有 PUBLISHED 状态才会阻塞同名发布,但区分 visibility。
| 对方状态 | 我发布同名 PRIVATE | 我发布同名 PUBLIC | 说明 |
|---------|-------------------|------------------|------|
| UPLOADED | ✅ 允许 | ✅ 允许 | 多个 UPLOADED 可共存 |
| PENDING_REVIEW | ✅ 允许 | ✅ 允许 | 还未正式发布 |
| PRIVATE + PUBLISHED | ❌ 拒绝 | ❌ 拒绝 | 只允许一个正式私有版本 |
| PUBLIC + PUBLISHED | ❌ 拒绝 | ❌ 拒绝 | 市场已占用 |
### 9.3 冲突规则表(详细)
| 场景 | 是否允许 | 说明 |
|------|---------|------|
| 同 namespace同 slug同 owner | 允许(复用) | 新版本挂到已有 skill 下 |
| 同 namespace同 slug不同 owner对方只有 UPLOADED | 允许 | 多个 UPLOADED 可共存测试 |
| 同 namespace同 slug不同 owner对方只有 PENDING_REVIEW | 允许 | 还未正式发布 |
| 同 namespace同 slug不同 owner对方有 PRIVATE + PUBLISHED | 拒绝 | 只允许一个正式私有版本 |
| 同 namespace同 slug不同 owner对方有 PUBLIC/NAMESPACE_ONLY + PUBLISHED | 拒绝 | 市场已占用 |
| 不同 namespace同 slug | 允许 | namespace 隔离 |
### 9.4 完整流程示例
```
用户 A 发布 PRIVATE `ns/my-skill`
状态UPLOADED
用户 B 发布 PRIVATE `ns/my-skill`
状态UPLOADED ✅ 允许(多个 UPLOADED 可共存)
用户 A 确认发布 → PRIVATE + PUBLISHED ✅ 允许
用户 B 确认发布 → ❌ 被拒绝
错误信息error.skill.publish.nameConflict.private
用户 B 可以:
1. 改名发布
2. 等用户 A 删除/归档后再发布
3. 提交审核变成 PUBLIC如果 A 是 PRIVATE
```
### 9.5 代码改动
**文件**`SkillPublishService.java`
```java
// 冲突检查逻辑(第 230-242 行)
for (Skill existing : existingSkills) {
if (!existing.getOwnerId().equals(publisherId)) {
// 检查是否有 PUBLISHED 版本
boolean hasPublished = !skillVersionRepository
.findBySkillIdAndStatus(existing.getId(), SkillVersionStatus.PUBLISHED)
.isEmpty();
if (hasPublished) {
// PUBLISHED 版本存在,无论 visibility 如何都拒绝
// 因为只允许一个 PRIVATE + PUBLISHED 或 PUBLIC + PUBLISHED
if (existing.getVisibility() == SkillVisibility.PRIVATE) {
throw new DomainBadRequestException("error.skill.publish.nameConflict.private", skillSlug);
} else {
throw new DomainBadRequestException("error.skill.publish.nameConflict", skillSlug);
}
}
}
}
```
### 9.6 错误信息
| 错误码 | 说明 |
|-------|------|
| `error.skill.publish.nameConflict` | 已有同名 PUBLIC/NAMESPACE_ONLY skill 发布 |
| `error.skill.publish.nameConflict.private` | 已有同名 PRIVATE skill 正式发布 |
---
## 10. package_name / runtime 规则
### 10.1 当前实现
- `package_name` 不是 Core 的结构化字段
- 存储在 `skill_version.parsedMetadataJson` JSONB 字段中
- 由 skill 作者在 SKILL.md frontmatter 中定义
### 10.2 SaaS Adapter 职责
-`parsedMetadataJson` 中提取 `package_name`
- 作为顶层字段返回给 AstronClaw
- 可选:检查跨 skill 的 package_name 唯一性
- 统一封装 `submit-review``confirm-publish`、删除、查询等 Core 能力,对 AstronClaw 暴露稳定接口
### 10.3 规则建议
| 规则 | 建议 |
|------|------|
| 格式 | 建议使用 `namespace__slug` 格式,避免冲突 |
| 跨版本稳定性 | 同一 skill 跨版本应保持 package_name 一致 |
| 唯一性 | SaaS Adapter 可检查并警告冲突,但不强制阻止 |
---
## 11. 代码改动清单
说明:
以下改动属于开源 `Core` 的规则实现,用于给 SaaS 封装层提供稳定能力基线;不等同于直接向 AstronClaw 暴露这些开源接口。
### 11.1 枚举新增
**文件**`SkillVersionStatus.java`
```java
public enum SkillVersionStatus {
DRAFT,
SCANNING,
SCAN_FAILED,
UPLOADED, // 新增
PENDING_REVIEW,
PUBLISHED,
REJECTED,
YANKED
}
```
### 11.2 发布逻辑改动
**文件**`SkillPublishService.java`
```java
// 第 279-285 行,改为
if (visibility == SkillVisibility.PRIVATE) {
version.setStatus(SkillVersionStatus.UPLOADED);
version.setPublishedAt(currentTime());
// 不创建审核任务
} else if (autoPublish) {
version.setStatus(SkillVersionStatus.PUBLISHED);
version.setPublishedAt(currentTime());
} else {
version.setStatus(SkillVersionStatus.PENDING_REVIEW);
// 创建审核任务
}
```
### 11.3 撤回审核改动
**文件**`SkillGovernanceService.java`
```java
// withdrawPendingVersion 方法,改为
skillVersion.setStatus(SkillVersionStatus.UPLOADED); // 原为 DRAFT
```
### 11.4 下载权限改动
**文件**`SkillDownloadService.java``SkillQueryService.java`
```java
// UPLOADED 和 PENDING_REVIEW 状态允许 owner 下载
private boolean canDownload(SkillVersion version, Skill skill, String currentUserId) {
return switch (version.getStatus()) {
case UPLOADED, PENDING_REVIEW -> skill.getOwnerId().equals(currentUserId);
case PUBLISHED -> true; // 按 visibility 判断
default -> false;
};
}
```
### 11.5 新增服务
**文件**`SkillReviewSubmitService.java`(新增)
- 实现 UPLOADED 版本提交审核逻辑
### 11.6 新增控制器
**文件**`SkillReviewSubmitController.java`(新增)
- 暴露 `POST /{namespace}/{slug}/submit-review` 接口
- 暴露 `POST /{namespace}/{slug}/confirm-publish` 接口
### 11.7 管理员可见性
**文件**`VisibilityChecker.java`
- SUPER_ADMIN 可以看到所有 skill包括 UPLOADED 状态
### 11.8 数据库迁移
**文件**:新增迁移脚本
- 更新 `skill_version_status` 枚举类型,添加 UPLOADED 值
---
## 12. 阻塞上线条件
| 问题 | 严重程度 | 状态 |
|------|---------|------|
| 新增 UPLOADED 状态 | 高 | 已完成 |
| PRIVATE skill 发布逻辑改动 | 高 | 已完成 |
| 提交审核接口 | 高 | 已完成 |
| 撤回审核后进入 UPLOADED | 中 | 已完成 |
| 同名冲突检查补全 | 中 | 已完成 |
| 管理员可见 UPLOADED skill | 低 | 已完成 |
| package_name 唯一性检查 | 低 | 可选SaaS Adapter 职责) |
---
## 13. 对老版本的影响
### 13.1 数据兼容性
| 影响点 | 分析 | 需要处理 |
|--------|------|---------|
| 老版本数据 | 不受影响,状态不变 | 否 |
| 数据库枚举 | 需添加 UPLOADED 值 | 是 |
| API 兼容性 | 新接口是新增,不影响老接口 | 否 |
### 13.2 状态流转影响
| 场景 | 老逻辑 | 新逻辑 | 影响 |
|------|--------|--------|------|
| 老版本撤回审核 | PENDING_REVIEW → DRAFT | PENDING_REVIEW → UPLOADED | 前端需适配新状态 |
| 老版本删除 | DRAFT/REJECTED/SCAN_FAILED 可删 | UPLOADED 也可删 | 需更新代码判断 |
### 13.3 代码改动点
**文件**`SkillGovernanceService.java`
**1. 删除版本逻辑**第163-166行
```java
// 原代码
if (version.getStatus() != SkillVersionStatus.DRAFT
&& version.getStatus() != SkillVersionStatus.REJECTED
&& version.getStatus() != SkillVersionStatus.SCAN_FAILED) {
throw new DomainBadRequestException("error.skill.version.delete.unsupported", version.getVersion());
}
// 改为:允许删除 UPLOADED 状态
if (version.getStatus() != SkillVersionStatus.DRAFT
&& version.getStatus() != SkillVersionStatus.REJECTED
&& version.getStatus() != SkillVersionStatus.SCAN_FAILED
&& version.getStatus() != SkillVersionStatus.UPLOADED) {
throw new DomainBadRequestException("error.skill.version.delete.unsupported", version.getVersion());
}
```
**2. 撤回审核逻辑**第245行
```java
// 原代码
version.setStatus(SkillVersionStatus.DRAFT);
// 改为
version.setStatus(SkillVersionStatus.UPLOADED);
```
### 13.4 前端适配
| 状态 | 前端展示建议 |
|------|-------------|
| UPLOADED | "已上传" 或 "待确认" |
| 可删除状态 | DRAFT、SCAN_FAILED、REJECTED、UPLOADED |
| 可编辑状态 | DRAFT、SCAN_FAILED、REJECTED |
### 13.5 迁移策略
1. **数据库迁移**:添加 UPLOADED 枚举值
2. **代码部署**:先部署后端,再部署前端
3. **老数据处理**:无需处理,老版本状态保持不变
4. **回滚方案**如需回滚UPLOADED 状态的版本按 DRAFT 处理