Skip to content

Commit f3dbbf8

Browse files
authored
refactor: skill lifecycle projection and docs (#78)
* Refactor skill lifecycle projection and docs * Improve owner lifecycle visibility on skill detail * Track download counts per skill version
1 parent 3a54a0b commit f3dbbf8

File tree

50 files changed

+1104
-393
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1104
-393
lines changed

docs/00-product-direction.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ ClawHub CLI 使用单一 slug 模型,slug 校验规则为 `[a-z0-9]([a-z0-9-]*
115115
- 团队技能提升到全局需平台管理员二次审核
116116
- 平台管理员只负责全局空间审核与提升审核,不介入团队空间审核
117117
- 当前不引入自动审核;`PrePublishValidator` 仅作为未来扩展点保留,默认实现为 `NoOp`
118+
- 撤回审核语义统一为 `PENDING_REVIEW → DRAFT`,不再走删除版本记录
119+
- skill 生命周期管理读模型统一为 `headlineVersion / publishedVersion / ownerPreviewVersion / resolutionMode`
120+
- `hidden` 是独立治理覆盖层,不属于 skill 容器状态机
118121

119122
认证与权限:
120123
- OAuth2 标准登录(一期 GitHub OAuth)

docs/02-domain-model.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@
6464
| owner_id | varchar(128) | 主要维护人(可转让) |
6565
| source_skill_id | bigint | 派生来源(团队技能提升到全局时记录原 skill ID),nullable |
6666
| visibility | enum | `PUBLIC` / `NAMESPACE_ONLY` / `PRIVATE` |
67-
| status | enum | `ACTIVE` / `HIDDEN` / `ARCHIVED` |
68-
| latest_version_id | bigint | 最新已发布版本(自动跟随,每次发布自动更新) |
67+
| status | enum | `ACTIVE` / `ARCHIVED` |
68+
| latest_version_id | bigint | latest published pointer,仅指向最新 `PUBLISHED` 版本;若不存在已发布版本则可为 `null` |
6969
| download_count | bigint | |
7070
| star_count | int | |
7171
| rating_avg | decimal(3,2) | 平均评分 |
@@ -76,6 +76,7 @@
7676
| updated_at | datetime | |
7777

7878
- 唯一约束:`(namespace_id, slug)`
79+
- `status` 表示 skill 容器生命周期,不再承载“隐藏”语义。隐藏是独立的治理覆盖层,由 `hidden` / `hidden_at` / `hidden_by` 表达
7980
- `owner_id` 语义为"主要维护人",可转让。权限主轴是 namespace role,不是 owner:
8081
- namespace ADMIN 对空间内所有 skill 有完整管理权(归档、版本管理、提升到全局),不受 owner 限制
8182
- owner 作为 MEMBER 时可管理自己创建的 skill(提交审核、编辑草稿)
@@ -102,8 +103,14 @@
102103
| published_at | datetime | |
103104
| created_at | datetime | |
104105

105-
- `status` 覆盖完整审核生命周期
106-
- 状态机:`DRAFT → PENDING_REVIEW → PUBLISHED / REJECTED``PUBLISHED → YANKED`
106+
- `status` 表示 version 发布生命周期,和 skill 容器状态、review task 状态分离
107+
- 当前代码下的实际迁移约束:
108+
- 普通用户首次上传/重传新版本后,版本直接进入 `PENDING_REVIEW`
109+
- `SUPER_ADMIN` 直发时可直接进入 `PUBLISHED`
110+
- 审核通过:`PENDING_REVIEW → PUBLISHED`
111+
- 审核拒绝:`PENDING_REVIEW → REJECTED`
112+
- 撤回审核:`PENDING_REVIEW → DRAFT`
113+
- 已发布撤回:`PUBLISHED → YANKED`
107114
- 唯一约束:`(skill_id, version)` 防止重复发布
108115
- `YANKED` 状态:已发布后撤回
109116

@@ -112,7 +119,7 @@
112119
| 版本状态 | 版本号处理 |
113120
|---------|-----------|
114121
| DRAFT | 可删除该版本记录,重新使用同版本号 |
115-
| PENDING_REVIEW | 可撤回到 DRAFT,然后删除 |
122+
| PENDING_REVIEW | 可撤回到 DRAFT |
116123
| REJECTED | 可删除该版本记录,重新使用同版本号 |
117124
| PUBLISHED | 版本号永久占用,不可复用 |
118125
| YANKED | 版本号永久占用,不可复用,版本列表中显示但标记为不可下载 |
@@ -144,7 +151,7 @@
144151
| updated_by | varchar(128) | |
145152
| updated_at | datetime | |
146153

147-
- `latest` 是系统保留标签,只读,自动跟随 `skill.latest_version_id`,不允许 API 手动移动
154+
- `latest` 是系统保留标签,只读,自动跟随 `skill.latest_version_id`;其语义严格等价于“最新已发布版本”,不允许 API 手动移动
148155
- 自定义标签(如 `beta``stable-2026q1`)允许人工创建和移动
149156
- 唯一约束:`(skill_id, tag_name)`
150157
- `target_version_id` 必须指向 `status = PUBLISHED` 的版本,应用层校验
@@ -166,7 +173,7 @@
166173

167174
- 仅用于普通发布审核,"提升到全局"使用独立的 `promotion_request`
168175
- `version` 字段用于乐观锁,防止多 Pod 并发审核
169-
- 业务约束:同一 `skill_version_id``status=PENDING` 时只能存在一条记录,重复提交返回 409 Conflict。撤回(PENDING → 删除 review_task + skill_version 回退到 DRAFT)后才能再次提交
176+
- 业务约束:同一 `skill_version_id``status=PENDING` 时只能存在一条记录,重复提交返回 409 Conflict。撤回时删除 `PENDING` review_task,并将 `skill_version` 回退到 `DRAFT`
170177
- PostgreSQL 并发约束落地:通过唯一索引 `(skill_version_id)` + 软删除标记实现。`review_task` 表增加 `deleted` 字段(bigint, 默认 0),唯一索引改为 `(skill_version_id, deleted)`。撤回时将 `deleted` 设为 `id`(非零值),新提交时 `deleted=0`,利用唯一索引防止并发重复提交。或者采用更简单的方案:撤回时物理删除 review_task 记录,依赖 `INSERT` 的唯一约束 `(skill_version_id)` 防并发。PostgreSQL 还支持 partial unique index 方案:`CREATE UNIQUE INDEX ON review_task (skill_version_id) WHERE status = 'PENDING'`,更优雅地实现"PENDING 状态唯一"约束
171178

172179
### promotion_request
@@ -293,7 +300,7 @@
293300
| 角色 code | 说明 | 典型权限 |
294301
|-----------|------|---------|
295302
| `SUPER_ADMIN` | 平台超管,拥有所有权限 | 全部 |
296-
| `SKILL_ADMIN` | 技能治理:全局空间审核、提升审核、隐藏/撤回 | `review:approve`, `skill:manage`, `promotion:approve` |
303+
| `SKILL_ADMIN` | 技能治理:全局空间审核、提升审核、隐藏/恢复、撤回已发布版本 | `review:approve`, `skill:manage`, `promotion:approve` |
297304
| `USER_ADMIN` | 用户治理:准入审批、封禁/解封、角色分配(不可分配 SUPER_ADMIN) | `user:manage`, `user:approve` |
298305
| `AUDITOR` | 审计只读:查看审计日志 | `audit:read` |
299306

@@ -341,7 +348,7 @@
341348

342349
### skill_search_document
343350

344-
一个 skill 对应一条搜索文档,内容取 `latest_version_id` 对应版本
351+
一个 skill 对应一条搜索文档,内容取“最新已发布版本”。实现上可由 `latest_version_id` 作为缓存指针承载,但其语义只能是 latest published pointer
345352

346353
| 字段 | 类型 | 说明 |
347354
|------|------|------|

docs/03-authentication-design.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ API Token 仍保留,但定位从“CLI 唯一认证方式”调整为“平台
377377
| 平台角色 | 职责 |
378378
|---------|------|
379379
| `SUPER_ADMIN` | 全部权限,硬判定短路 |
380-
| `SKILL_ADMIN` | 全局空间审核、提升审核、隐藏/撤回技能 |
380+
| `SKILL_ADMIN` | 全局空间审核、提升审核、隐藏/恢复技能、撤回已发布版本 |
381381
| `USER_ADMIN` | 准入审批、封禁/解封、角色分配(不可分配 SUPER_ADMIN) |
382382
| `AUDITOR` | 审计日志只读 |
383383

@@ -402,7 +402,8 @@ API Token 仍保留,但定位从“CLI 唯一认证方式”调整为“平台
402402
| 审核团队空间技能 | `review:approve` | 该 namespace 的 ADMIN 或 OWNER |
403403
| 审核全局空间技能 | `review:approve` | 持有 SKILL_ADMIN / SUPER_ADMIN |
404404
| 审核提升申请 | `promotion:approve` | 持有 SKILL_ADMIN / SUPER_ADMIN |
405-
| 隐藏/撤回技能 | `skill:manage` | 持有 SKILL_ADMIN / SUPER_ADMIN |
405+
| 隐藏/恢复技能 | `skill:manage` | 持有 SKILL_ADMIN / SUPER_ADMIN |
406+
| 撤回已发布版本(YANK) | `skill:manage` | 持有 SKILL_ADMIN / SUPER_ADMIN |
406407
| 管理用户角色 | `user:manage` | 持有 USER_ADMIN / SUPER_ADMIN |
407408
| 审批用户准入 | `user:approve` | 持有 USER_ADMIN / SUPER_ADMIN |
408409
| 查看审计日志 | `audit:read` | 持有 AUDITOR / SUPER_ADMIN |
@@ -600,7 +601,7 @@ window.location.href = '/oauth2/authorization/github'
600601
| `POST /api/v1/skills/{ns}/{slug}/star` | 已登录 | Session/Token |
601602
| `POST /api/v1/skills/{ns}/{slug}/rating` | 已登录 | Session/Token |
602603
| `POST .../versions/{ver}/submit-review` | namespace MEMBER 以上 | `namespace_member.role` |
603-
| `POST .../versions/{ver}/withdraw-review` | 提交人本人 或 namespace ADMIN | `review_task.submitted_by``namespace_member.role` |
604+
| `POST .../versions/{ver}/withdraw-review` | 提交人本人 | `review_task.submitted_by` |
604605
| `PUT /api/v1/skills/{ns}/{slug}/tags/{tag}` | namespace ADMIN 以上 或 owner | `namespace_member.role``skill.owner_id` |
605606
| `POST /api/v1/skills/{ns}/{slug}/archive` | namespace ADMIN 以上 或 owner | `namespace_member.role``skill.owner_id` |
606607
| `DELETE .../versions/{ver}` | namespace ADMIN 以上 或 owner(仅 DRAFT/REJECTED) | `namespace_member.role``skill.owner_id` + `skill_version.status` |

docs/04-search-architecture.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ WHERE (visibility = 'PUBLIC')
5757

5858
## 3 搜索文档表 skill_search_document
5959

60-
一个 skill 对应一条搜索文档,内容取 `latest_version_id` 对应版本。版本发布时自动更新该条文档
60+
一个 skill 对应一条搜索文档,但文档内容的来源语义应严格收敛为“当前最新已发布版本”。实现上仍可由 `latest_version_id` 作为缓存指针承载,但它只允许指向 `PUBLISHED` 版本;搜索层不能再把它当作泛化的“当前版本”
6161

6262
| 字段 | 类型 | 说明 |
6363
|------|------|------|
@@ -80,14 +80,15 @@ PostgreSQL 全文搜索索引:表增加 `search_vector tsvector` 生成列,
8080
## 4 索引写入时机
8181

8282
以下场景触发搜索文档更新(upsert by skill_id):
83-
- 审核通过(`PENDING_REVIEW → PUBLISHED`):`latest_version_id` 自动更新,用新版本内容更新搜索文档
83+
- 审核通过(`PENDING_REVIEW → PUBLISHED`):重算“最新已发布版本”指针,并用该发布版本内容更新搜索文档
84+
- 已发布版本被撤回(`PUBLISHED → YANKED`):重算“最新已发布版本”指针;若不存在任何已发布版本,则移除搜索文档
8485
- 技能状态变更(隐藏/归档/恢复):更新搜索文档的 status 字段
8586

8687
## 5 搜索演进路线
8788

8889
### 5.1 一期数据建模约束
8990

90-
一期"每个 skill 一条搜索文档、内容永远取 latest_version_id"是有意的简化。这个模型在以下场景下会不够用:
91+
一期每个 skill 一条搜索文档、内容永远取最新已发布版本”是有意的简化。当前实现仍使用 `latest_version_id` 作为持久化指针,但这里的语义已经收敛为 latest published pointer。这个模型在以下场景下会不够用:
9192

9293
- 版本级检索(搜索某个旧版本的内容)
9394
- 自定义标签/通道检索(搜索 `@beta` 标签指向的版本内容)
@@ -96,7 +97,7 @@ PostgreSQL 全文搜索索引:表增加 `search_vector tsvector` 生成列,
9697
这些场景不是简单换 provider 能解决的,需要改表结构和索引写入逻辑。
9798

9899
**一期搜索能力边界(产品限制):**
99-
- 搜索只基于 `latest_version_id` 对应版本的内容
100+
- 搜索只基于“最新已发布版本”的内容
100101
- 不支持按 version 或 tag 搜索内容
101102
- 搜索结果不区分 channel(`beta``stable` 等标签通道)
102103
- 用户通过 tag 安装的技能内容可能与搜索结果展示的内容不一致(搜索展示 latest,安装的是 tag 指向的版本)
@@ -106,8 +107,8 @@ PostgreSQL 全文搜索索引:表增加 `search_vector tsvector` 生成列,
106107

107108
| 阶段 | 实现 | 索引粒度 | 切换方式 |
108109
|------|------|---------|---------|
109-
| 一期 | PostgreSQL Full-Text (tsvector + GIN) | 每 skill 一条(latest_version_id| 默认 |
110-
| 一点五期 | PostgreSQL Full-Text + 语义向量重排 | 每 skill 一条(latest_version_id| 配置 `skillhub.search.semantic.enabled=true` |
110+
| 一期 | PostgreSQL Full-Text (tsvector + GIN) | 每 skill 一条(latest published| 默认 |
111+
| 一点五期 | PostgreSQL Full-Text + 语义向量重排 | 每 skill 一条(latest published| 配置 `skillhub.search.semantic.enabled=true` |
111112
| 二期 | ES / OpenSearch | 每 skill_version 一条 + skill 聚合文档 | 配置 `search.provider=elasticsearch` |
112113
| 三期 | 向量检索 | 每 skill_version 多条(chunk 级) | 配置 `search.provider=vector` |
113114
| 四期 | 混合排序 | 关键词 + 向量混合 | 配置 `search.provider=hybrid` |

docs/05-business-flows.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,26 @@
4848
- 同步创建 `review_task(status=PENDING)`
4949
- 审核通过后转为 `PUBLISHED`
5050
- 审核拒绝后转为 `REJECTED`
51+
- 撤回审核时删除 `PENDING review_task`,并将 `skill_version` 回退到 `DRAFT`
5152
- 例外:提交人持有 `SUPER_ADMIN` 平台角色时,发布入口直接创建 `skill_version(status=PUBLISHED)`,跳过 `review_task` 创建,同时不再要求其必须是目标 namespace 成员
5253
- 上述例外必须对 Web、`/api/v1/publish``/api/v1/publish` 保持一致
54+
- 若重传新版本时发现旧的 `PENDING_REVIEW` 版本,旧版本会被自动降回 `DRAFT`,再创建新的待审版本
55+
56+
### 1.2 生命周期读模型
57+
58+
当前代码中的 skill 生命周期展示与操作判断,不再依赖旧的 `latestVersionStatus``viewingVersionStatus` 一类拼装字段,而统一基于以下 projection:
59+
60+
- `headlineVersion`:当前详情页/我的技能列表主展示版本
61+
- `publishedVersion`:当前最新可公开分发的已发布版本
62+
- `ownerPreviewVersion`:owner 或 namespace 管理者可见的待审核版本
63+
- `resolutionMode``PUBLISHED` / `OWNER_PREVIEW` / `NONE`
64+
65+
业务规则:
66+
67+
- 公开入口只认 `publishedVersion`
68+
- owner 进入详情页时,如果没有可用 `publishedVersion`,才允许 `headlineVersion = ownerPreviewVersion`
69+
- 推广到全局、安装命令、公开下载都只能绑定到 `publishedVersion`
70+
- `hidden` 是独立治理覆盖层,不属于 skill 生命周期状态机
5371

5472
### 对象存储写入策略
5573

@@ -121,6 +139,11 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确
121139
- 原团队 skill 可继续独立迭代
122140
- 两者版本不自动同步,如需同步由 owner 手动操作
123141

142+
提升流程当前严格绑定已发布版本:
143+
144+
- promotion request 的 `source_version_id` 必须指向 `publishedVersion.id`
145+
- 不允许直接提升 `ownerPreviewVersion`
146+
124147
## 3 下载流程
125148

126149
```

docs/06-api-design.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}` | 版本详情 |
7777
| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}/files` | 文件清单 |
7878
| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}/file?path=...` | 读取单个文件(query param 避免路径中 / 的解析问题) |
79-
| GET | `/api/v1/skills/{namespace}/{slug}/download` | 下载默认安装版本(latest_version_id 指向的版本|
79+
| GET | `/api/v1/skills/{namespace}/{slug}/download` | 下载默认安装版本(最新已发布版本|
8080
| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}/download` | 下载指定版本包 |
8181
| GET | `/api/v1/skills/{namespace}/{slug}/resolve` | 解析技能版本(支持 query param: `version``tag``hash`|
8282
| GET | `/api/v1/skills/{namespace}/{slug}/tags/{tagName}/download` | 按标签下载(解析标签指向的版本后下载) |
@@ -206,7 +206,7 @@ Public API 的可见性规则:
206206

207207
| 方法 | 路径 | 说明 |
208208
|------|------|------|
209-
| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/submit-review` | 将 DRAFT 版本提交审核 |
209+
| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/submit-review` |`DRAFT` 版本再次提交审核(当前主要用于撤回后重提) |
210210
| POST | `/api/v1/skills/{namespace}/{slug}/versions/{version}/withdraw-review` | 撤回提审(PENDING_REVIEW → DRAFT,同时删除关联的 PENDING review_task) |
211211
| GET | `/api/v1/skills/{namespace}/{slug}/versions/{version}/draft` | 查看草稿详情(owner 或 namespace ADMIN 以上) |
212212

@@ -226,6 +226,20 @@ Public API 的可见性规则:
226226
| POST | `/api/v1/skills/{namespace}/{slug}/unarchive` | 恢复归档(namespace ADMIN 或 owner) |
227227
| DELETE | `/api/v1/skills/{namespace}/{slug}/versions/{version}` | 删除 DRAFT/REJECTED 版本 |
228228

229+
当前代码中的 skill 生命周期读模型不再依赖 `latestVersionStatus` / `viewingVersionStatus` 一类拼装字段,而统一使用以下 projection:
230+
231+
- `headlineVersion`:当前页面应展示的主版本
232+
- `publishedVersion`:当前最新可分发的已发布版本
233+
- `ownerPreviewVersion`:owner / namespace 管理者可见的待审核预览版本
234+
- `resolutionMode``PUBLISHED` / `OWNER_PREVIEW` / `NONE`
235+
236+
其中:
237+
238+
- 公开详情、公开安装、公开搜索一律只认 `publishedVersion`
239+
- owner 详情页在没有可展示发布版本时,才允许 `headlineVersion` 落到 `ownerPreviewVersion`
240+
- 推广到全局一律使用 `publishedVersion.id`
241+
- `hidden` 是独立治理覆盖层,不属于生命周期状态机
242+
229243
发布成功响应中的 `data` 至少包含以下字段:
230244

231245
- `skillId`
@@ -241,6 +255,7 @@ Public API 的可见性规则:
241255
- 普通用户发布成功后,`status``PENDING_REVIEW`
242256
- 持有 `SUPER_ADMIN` 的用户通过 Web、`/api/v1/publish``/api/v1/publish` 发布时,`status``PUBLISHED`,且不要求其必须是目标 namespace 成员
243257
- 当前版本保持该审核策略,不再提供“全员直发”的运行模式
258+
- 撤回审核不会删除版本记录,而是 `PENDING_REVIEW → DRAFT`
244259

245260
## 7.4 Token API(需登录)
246261

@@ -336,14 +351,15 @@ Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN:
336351
`latest` 自动跟随最新已发布版本,不可手动移动。
337352

338353
- `skill.latest_version_id`:每次审核通过自动更新,始终指向最新 PUBLISHED 版本
354+
- `yank` 当前最新已发布版本时,需要同步重算 `latest_version_id` 指向下一个最新的 `PUBLISHED` 版本;若不存在则允许为 `null`
339355
- `latest` 标签:系统保留,只读,自动与 `latest_version_id` 同步
340356
- 自定义标签(如 `beta``stable-2026q1`):允许人工创建和移动,用于固定安装通道
341357

342358
| 场景 | 使用字段 | 说明 |
343359
|------|---------|------|
344-
| 搜索索引内容 | `latest_version_id` | 搜索文档取最新已发布版本内容 |
345-
| `/download`(不带版本号) | `latest_version_id` | 下载最新已发布版本 |
346-
| CLI `install @team/skill` | `latest_version_id` | 等同于 `@latest` |
360+
| 搜索索引内容 | `publishedVersion` / `latest_version_id` | 外部协议仍叫 latest,但内部语义必须等价于最新已发布版本 |
361+
| `/download`(不带版本号) | `publishedVersion` / `latest_version_id` | 下载最新已发布版本 |
362+
| CLI `install @team/skill` | `publishedVersion` / `latest_version_id` | 等同于 `@latest` |
347363
| CLI `install @team/skill@beta` | `skill_tag` 查询 | 自定义标签指向的版本 |
348364

349365
## 7.9 Resolve 接口说明
@@ -361,7 +377,7 @@ Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN:
361377
2. 仅传 `version`:精确匹配版本号
362378
3. 仅传 `tag`:查询 `skill_tag` 表获取 `target_version_id`
363379
4. 仅传 `hash`:遍历已发布版本,比对 fingerprint
364-
5. 均不传:返回 `latest_version_id` 指向的版本
380+
5. 均不传:返回最新已发布版本;实现上可由 `latest_version_id` 或等价 published projection 解析
365381

366382
响应:
367383

docs/07-skill-protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ CLI 安装后在本地写入 `.astron/metadata.json`:
116116
skillhub 自有 CLI 支持完整 namespace 坐标:
117117

118118
```
119-
install @team/my-skill → 最新已发布版本(latest_version_id)
119+
install @team/my-skill → 最新已发布版本(实现上通常由 `latest_version_id` / published pointer 解析
120120
install @team/my-skill@1.2.0 → 精确版本
121121
install @team/my-skill@latest → 等同于不带版本号(系统保留标签,只读)
122122
install @team/my-skill@beta → beta 标签(自定义标签)

0 commit comments

Comments
 (0)