From 22177a46e9edf851cede0580508d1a50d658fb5d Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:11:53 +0800 Subject: [PATCH 1/2] docs(lifecycle): align lifecycle contracts with verified behavior Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .trellis/spec/backend/api-contracts.md | 10 +- docs/02-domain-model.md | 43 +++----- docs/04-search-architecture.md | 4 +- docs/05-business-flows.md | 140 ++++++++++--------------- docs/06-api-design.md | 37 ++++--- docs/08-frontend-architecture.md | 2 +- docs/14-skill-lifecycle.md | 96 ++++++++++------- 7 files changed, 156 insertions(+), 176 deletions(-) diff --git a/.trellis/spec/backend/api-contracts.md b/.trellis/spec/backend/api-contracts.md index 4e4c09e3..ca76665d 100644 --- a/.trellis/spec/backend/api-contracts.md +++ b/.trellis/spec/backend/api-contracts.md @@ -34,10 +34,10 @@ Mock 身份仅按现有开发 profile 使用,不替代真实认证验收。 3. 选择 HTTP/消费方测试验证实际响应和失败路径;至少覆盖此次变化触及的身份/权限边界。 4. [生成漂移脚本](../../../scripts/check-openapi-generated.sh) 会启动依赖并在退出时执行 Compose down;仅在隔离、可清理的本地环境运行,不能作为无副作用的文档检查。 -## 已知文档漂移 +## 生命周期对齐与剩余例外 -2026-10-15 静态核查:docs/14 的撤回描述仍为 DRAFT,而 -[withdrawPendingVersion 实现](../../../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java) 设置 UPLOADED。 -旧工程 Skill 的状态表也不是新的权威。修改相关功能时对照 -[治理测试](../../../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java)和正式需求解决差异;本次迁移不裁定新业务状态。 +2026-10-15 已将[生命周期文档](../../../docs/14-skill-lifecycle.md)的撤回目标对齐为 `UPLOADED`, +并补充扫描、PRIVATE 确认、删除与访问约束;依据正式语义、实现及测试,不沿用旧工程 Skill 状态表。 +[Latest 章节](../../../docs/14-skill-lifecycle.md#31-latest)仍记录原始 `latestVersionId` 在 PRIVATE 上传中的未发布指针例外; +是否统一持久化不变量需另行设计,不能因此放宽公共分发仅使用 published 版本的约束。 发布审核绕过、可见性、latest 指针、警告确认、删除约束均须核对当前服务与测试,不从历史快照复制规则。 diff --git a/docs/02-domain-model.md b/docs/02-domain-model.md index 45efbc0b..d6e935f2 100644 --- a/docs/02-domain-model.md +++ b/docs/02-domain-model.md @@ -65,7 +65,7 @@ | source_skill_id | bigint | 派生来源(团队技能提升到全局时记录原 skill ID),nullable | | visibility | enum | `PUBLIC` / `NAMESPACE_ONLY` / `PRIVATE` | | status | enum | `ACTIVE` / `ARCHIVED` | -| latest_version_id | bigint | latest published pointer,仅指向最新 `PUBLISHED` 版本;若不存在已发布版本则可为 `null` | +| latest_version_id | bigint | 内部生命周期指针;PRIVATE 上传可指向未发布版本。对外最新发布版本由 `publishedVersion` 解析,不直接以此字段授权 | | download_count | bigint | | | star_count | int | | | rating_avg | decimal(3,2) | 平均评分 | @@ -77,7 +77,7 @@ - 唯一约束:`(namespace_id, slug, owner_id)`;面向公众的同名冲突还须按已发布记录检查不同 owner,避免地址解析歧义 - `status` 表示 skill 容器生命周期,不再承载“隐藏”语义。隐藏是独立的治理覆盖层,由 `hidden` / `hidden_at` / `hidden_by` 表达 -- 当前代码下的实际可见性判定以 `VisibilityChecker` 为准,规则如下: +- 容器可见性判定以 `VisibilityChecker` 为准;传入 `SUPER_ADMIN` 平台角色时该检查短路放行,但不表示所有调用方都传入角色,也不绕过调用方的其他限制。普通调用规则如下: - 若 `hidden=true`:仅 skill owner 或该 namespace 的 `ADMIN` / `OWNER` 可读 - 若 `latest_version_id is null`:仅 skill owner 可读;即使 `visibility=PUBLIC` 也不会对外公开 - `PUBLIC`:任意人可读 skill 容器与已发布版本 @@ -103,38 +103,19 @@ | changelog | text | | | manifest_json | json | 文件清单 | | parsed_metadata_json | json | SKILL.md frontmatter 解析结果 | -| status | enum | `DRAFT` / `PENDING_REVIEW` / `PUBLISHED` / `REJECTED` / `YANKED` | +| status | enum | `DRAFT` / `SCANNING` / `SCAN_FAILED` / `UPLOADED` / `PENDING_REVIEW` / `PUBLISHED` / `REJECTED` / `YANKED` | | reject_reason | varchar(512) | 拒绝原因 | | published_by | varchar(128) | | | published_at | datetime | | | created_at | datetime | | - `status` 表示 version 发布生命周期,和 skill 容器状态、review task 状态分离 -- 当前代码下的实际迁移约束: - - 普通用户首次上传/重传新版本后,版本直接进入 `PENDING_REVIEW` - - `SUPER_ADMIN` 直发时可直接进入 `PUBLISHED` - - 审核通过:`PENDING_REVIEW → PUBLISHED` - - 审核拒绝:`PENDING_REVIEW → REJECTED` - - 撤回审核:`PENDING_REVIEW → DRAFT` - - 已发布撤回:`PUBLISHED → YANKED` -- 唯一约束:`(skill_id, version)` 防止重复发布 -- `YANKED` 状态:已发布后撤回 -- 当前代码下的实际读权限补充: - - 普通详情 / 下载 / resolve / tag / 文件读取,只接受 `PUBLISHED` - - owner 可通过常规版本详情预览自己的 `PENDING_REVIEW` 版本 - - owner / namespace `ADMIN` / `OWNER` 在版本列表中可看到全部五种状态:`PUBLISHED / PENDING_REVIEW / DRAFT / REJECTED / YANKED` - - 但常规版本详情接口并不会放行 `DRAFT / REJECTED / YANKED` - - 审核详情页走独立 review 读路径,可查看待审版本及完整版本快照 - -版本号不可变性规则: - -| 版本状态 | 版本号处理 | -|---------|-----------| -| DRAFT | 可删除该版本记录,重新使用同版本号 | -| PENDING_REVIEW | 可撤回到 DRAFT | -| REJECTED | 可删除该版本记录,重新使用同版本号 | -| PUBLISHED | 版本号永久占用,不可复用 | -| YANKED | 版本号永久占用,不可复用,版本列表中显示但标记为不可下载 | +- 完整状态定义、上传/扫描/审核转换和治理操作以 [生命周期文档](./14-skill-lifecycle.md) 为准;`DRAFT` 保留为旧数据兼容状态,当前撤回审核回到 `UPLOADED`。 +- 上传结果取决于请求可见性、扫描配置与发布权限,不能统一描述为“普通用户直接待审”或“超管发布即代表可安装”。 +- 唯一约束:`(skill_id, version)` 防止重复版本记录;已发布版本不可原地覆盖。允许删除的未发布版本删除后可复用版本号,但不能删除技能的最后一个版本。 +- `YANKED` 表示已发布后撤回,版本记录和版本号仍保留,不可下载。 +- 版本列表、详情/文件预览、下载与公共分发是不同权限路径:管理者能预览的版本不一定能下载,`UPLOADED` / `PENDING_REVIEW` 的受限下载也不代表对外发布。具体边界见 [生命周期文档的对外协议与权限章节](./14-skill-lifecycle.md)。 +- 审核详情使用独立 review 读路径,可查看审核快照;不能由普通详情的访问规则推导审核权限。 ### skill_file @@ -163,7 +144,7 @@ | updated_by | varchar(128) | | | updated_at | datetime | | -- `latest` 是系统保留标签,只读,自动跟随 `skill.latest_version_id`;其语义严格等价于“最新已发布版本”,不允许 API 手动移动 +- `latest` 是系统保留标签,只读,由发布和撤回流程维护,不允许 API 手动移动;PRIVATE 上传更新内部 `latest_version_id` 不代表已更新公开标签 - 自定义标签(如 `beta`、`stable-2026q1`)允许人工创建和移动 - 唯一约束:`(skill_id, tag_name)` - `target_version_id` 必须指向 `status = PUBLISHED` 的版本,应用层校验 @@ -185,7 +166,7 @@ - 仅用于普通发布审核,"提升到全局"使用独立的 `promotion_request` 表 - `version` 字段用于乐观锁,防止多 Pod 并发审核 -- 业务约束:同一 `skill_version_id` 在 `status=PENDING` 时只能存在一条记录,重复提交返回 409 Conflict。撤回时删除 `PENDING` review_task,并将 `skill_version` 回退到 `DRAFT` +- 业务约束:同一 `skill_version_id` 在 `status=PENDING` 时只能存在一条记录,重复提交返回 409 Conflict。撤回时删除 `PENDING` review_task,并将 `skill_version` 回退到 `UPLOADED` - 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 状态唯一"约束 ### promotion_request @@ -383,7 +364,7 @@ ### skill_search_document -一个 skill 对应一条搜索文档,内容取“最新已发布版本”。实现上可由 `latest_version_id` 作为缓存指针承载,但其语义只能是 latest published pointer。 +一个 skill 对应一条搜索文档,内容取 `publishedVersion` 表示的“最新已发布版本”。内部 `latest_version_id` 可能在 PRIVATE 上传时指向未发布版本,不能不校验状态就用于公开搜索。 | 字段 | 类型 | 说明 | |------|------|------| diff --git a/docs/04-search-architecture.md b/docs/04-search-architecture.md index e40d6da4..dd208b1a 100644 --- a/docs/04-search-architecture.md +++ b/docs/04-search-architecture.md @@ -57,7 +57,7 @@ WHERE (visibility = 'PUBLIC') ## 3 搜索文档表 skill_search_document -一个 skill 对应一条搜索文档,但文档内容的来源语义应严格收敛为“当前最新已发布版本”。实现上仍可由 `latest_version_id` 作为缓存指针承载,但它只允许指向 `PUBLISHED` 版本;搜索层不能再把它当作泛化的“当前版本”。 +一个 skill 对应一条搜索文档,但文档内容的来源语义应严格收敛为“当前最新已发布版本”。搜索消费方必须验证发布与可安装条件,不能仅凭 `latest_version_id` 非空认定可公开分发;该原始指针存在 PRIVATE 上传例外,见[生命周期 Latest 约束](14-skill-lifecycle.md#31-latest)。 | 字段 | 类型 | 说明 | |------|------|------| @@ -88,7 +88,7 @@ PostgreSQL 全文搜索索引:表增加 `search_vector tsvector` 生成列, ### 5.1 一期数据建模约束 -一期“每个 skill 一条搜索文档、内容永远取最新已发布版本”是有意的简化。当前实现仍使用 `latest_version_id` 作为持久化指针,但这里的语义已经收敛为 latest published pointer。这个模型在以下场景下会不够用: +一期“每个 skill 一条搜索文档、内容永远取最新已发布版本”是有意的简化。公开搜索的 latest 语义是 latest published;原始持久化指针的例外不能改变这个公开契约。这个模型在以下场景下会不够用: - 版本级检索(搜索某个旧版本的内容) - 自定义标签/通道检索(搜索 `@beta` 标签指向的版本内容) diff --git a/docs/05-business-flows.md b/docs/05-business-flows.md index f809c531..425f303c 100644 --- a/docs/05-business-flows.md +++ b/docs/05-business-flows.md @@ -2,9 +2,7 @@ ## 1 发布流程 -一期采用同步发布模型:上传、校验、存储、持久化在一次请求中同步完成。前端通过异步上传(带进度条)提升用户体验,但后端处理是同步的。 - -> **设计决策**:一期暂不考虑异步发布(uploadId、publishId、状态轮询、异步转正等)。一期技能包为文本资源包,体积有限(上限 10MB),同步处理足以满足需求。如后续引入大文件或复杂校验流程,再考虑异步模型。 +上传、包校验、对象存储写入和数据库持久化在一次请求中同步完成;接收成功不等于安全扫描和人工审核已经完成。扫描启用时,上传会触发后续扫描,包含直发版本;`PUBLIC` / `NAMESPACE_ONLY` 上传要求扫描器启用。具体状态及分发条件见[生命周期文档](./14-skill-lifecycle.md)。 ### 1.1 当前发布流程基线 @@ -29,98 +27,71 @@ ▼ ④ 持久化数据 - 创建或关联 skill 记录(首次发布时创建 skill) - - 创建 skill_version(普通用户进入 `PENDING_REVIEW`,`SUPER_ADMIN` 直达 `PUBLISHED`) + - 创建 skill_version(普通 PRIVATE 为 `UPLOADED`;普通 PUBLIC/NAMESPACE_ONLY 为 `PENDING_REVIEW`;直发为 `PUBLISHED`) - 创建 skill_file 记录 - 解析 SKILL.md frontmatter → parsed_metadata_json - 生成 manifest_json - - 直发场景更新 skill.latest_version_id + - 直发及 PRIVATE 上传更新 skill.latest_version_id(指针非空不代表可公开分发) │ ▼ ⑤ 同步写入审计日志 │ ▼ -⑥ 异步触发搜索索引写入 +⑥ 按发布分支触发后续处理:扫描启用时触发安全扫描;直发产生发布事件,待审版本等待审核 ``` -当前版本采用审核流,不再区分“Phase 2 直发”与“Phase 3 恢复审核”两套现实实现: +以下是版本创建时的初始分支;扫描启用时,返回前触发扫描会将非发布版本设为 `SCANNING`,扫描完成后才回到对应的 `UPLOADED` 或 `PENDING_REVIEW`。初始状态不等于响应状态: -- 普通用户发布请求创建 `skill_version(status=PENDING_REVIEW)` -- 同步创建 `review_task(status=PENDING)` -- 审核通过后转为 `PUBLISHED` -- 审核拒绝后转为 `REJECTED` -- 撤回审核时删除 `PENDING review_task`,并将 `skill_version` 回退到 `DRAFT` -- 例外:提交人持有 `SUPER_ADMIN` 平台角色时,发布入口直接创建 `skill_version(status=PUBLISHED)`,跳过 `review_task` 创建,同时不再要求其必须是目标 namespace 成员 -- 上述例外必须对 Web、`/api/v1/publish`、`/api/v1/publish` 保持一致 -- 若重传新版本时发现旧的 `PENDING_REVIEW` 版本,旧版本会被自动降回 `DRAFT`,再创建新的待审版本 +- 普通 `PRIVATE` 上传创建 `UPLOADED` 版本,不创建审核任务;保存私有版本不等于公开发布。 +- 普通 `PUBLIC` / `NAMESPACE_ONLY` 上传创建 `PENDING_REVIEW` 版本及 `PENDING` 审核任务。 +- 审核通过转为 `PUBLISHED`,审核拒绝转为 `REJECTED`;扫描与可分发条件仍需分别检查。 +- 撤回审核将 `PENDING_REVIEW` 版本变为 `UPLOADED`,删除关联待审任务。 +- 提交人持有 `SUPER_ADMIN` 时,上传直接创建 `PUBLISHED`,跳过审核任务及目标 namespace 成员检查;内部 `forceAutoPublish` 路径也可直发。直发不等于跳过所有安全扫描与下载就绪检查。 +- Web 与 CLI 发布入口共用上述发布语义。 +- 普通重传遇到旧 `PENDING_REVIEW` 版本时,先撤回旧审核并将其改为 `UPLOADED`,再按新请求的可见性及直发条件创建版本。 ### 1.2 生命周期读模型 当前代码中的 skill 生命周期展示与操作判断,不再依赖旧的 `latestVersionStatus`、`viewingVersionStatus` 一类拼装字段,而统一基于以下 projection: - `headlineVersion`:当前详情页/我的技能列表主展示版本 -- `publishedVersion`:当前最新可公开分发的已发布版本 -- `ownerPreviewVersion`:详情 projection 中仅暴露给 owner / namespace 管理者的 `PENDING_REVIEW` 版本 +- `publishedVersion`:当前已发布版本投影;详情投影与公开列表的就绪过滤存在区别,不能只凭字段非空判定可下载 +- `ownerPreviewVersion`:详情 projection 中暴露给 owner / namespace 管理者的新一轮非公开版本;排除 `PUBLISHED`、`YANKED`,按创建时间及 ID 选择最新,且须比已有已发布版本更新 - `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE` 业务规则: -- 公开入口只认 `publishedVersion` +- 公开入口以已发布版本为分发候选;`publishedVersion` 不替代各入口的授权与就绪校验 - owner 进入详情页时,如果没有可用 `publishedVersion`,才允许 `headlineVersion = ownerPreviewVersion` -- 推广到全局、安装命令、公开下载都只能绑定到 `publishedVersion` +- 推广、安装及公开分发不能使用未发布预览版本;具体候选版本与下载就绪条件见[生命周期文档](./14-skill-lifecycle.md) - `hidden` 是独立治理覆盖层,不属于 skill 生命周期状态机 -### 1.3 Skill 可见性与角色访问矩阵 +### 1.3 读取与操作边界 -以下矩阵以当前后端实现为准,综合了 `VisibilityChecker`、`SkillQueryService`、`SkillDownloadService`、`ReviewPermissionChecker` 的实际行为。 +完整状态、角色与端点差异以[生命周期文档](./14-skill-lifecycle.md)为准,不能把详情、文件预览、下载、审核与公开解析合并为一张通用权限矩阵: -#### 1.3.1 Skill 容器读取 +| 场景 | 需要分别检查的边界 | +|------|--------------------| +| Skill 容器读取 | 可见性、hidden、容器状态及读取入口授权。`latest_version_id` 为空时的限制不能等同于“无 publishedVersion”;PRIVATE 上传也可能将指针设为 `UPLOADED` | +| 版本列表与详情预览 | 先通过容器授权;管理视角可访问非公开版本,不限于 `PENDING_REVIEW`;详情可读不表示可下载 | +| 常规下载 | 要求容器 ACTIVE、通过可见性与 namespace 检查;`PUBLISHED` 还须 downloadReady 且未 yank;`UPLOADED` / `PENDING_REVIEW` 仅允许 owner 或 namespace ADMIN/OWNER 下载 | +| 公开解析与安装 | 使用已发布且符合分发条件的版本,不把 ownerPreviewVersion 当作公开候选 | +| 审核详情与审核包 | 独立审核权限入口;审核绑定下载不计入公开下载量,不能套用普通详情或普通下载的权限结论 | +| 治理与提审 | 提审、撤回审核、归档、隐藏、yank 与删除是不同动作;平台角色是否穿透需按实际入口判断,不能从一个入口推广到所有读取路径 | -| 角色 | PUBLIC | NAMESPACE_ONLY | PRIVATE | hidden 任意 visibility | 无 `publishedVersion`(`latest_version_id=null`) | -|------|--------|----------------|---------|------------------------|-----------------------------------------------| -| 匿名用户 | 可读 | 不可读 | 不可读 | 不可读 | 不可读 | -| 登录非成员 | 可读 | 不可读 | 不可读 | 不可读 | 不可读 | -| namespace MEMBER | 可读 | 可读 | 不可读 | 不可读 | 仅自己是 owner 时可读 | -| skill owner | 可读 | 可读 | 可读 | 可读 | 可读 | -| namespace ADMIN / OWNER | 可读 | 可读 | 可读 | 可读 | 不可读,除非本人也是 skill owner | -| SKILL_ADMIN / SUPER_ADMIN(仅平台角色) | 与普通登录用户一致;普通读路径不会因为平台角色自动穿透 private / hidden / unpublished | - -补充: -- `hidden=true` 时,可读权限会收敛为“skill owner 或 namespace `ADMIN` / `OWNER`” -- `visibility=PUBLIC` 也不意味着未发布 skill 可见;当 `latest_version_id` 为空时,只有 owner 能读 - -#### 1.3.2 Version 状态读取 - -| 场景 / 角色 | DRAFT | PENDING_REVIEW | PUBLISHED | REJECTED | YANKED | -|------------|-------|----------------|-----------|----------|--------| -| 普通 skill 详情页主版本投影 | 不展示 | owner / namespace 管理者可作为 `ownerPreviewVersion` 展示 | 展示 | 不展示 | 不展示 | -| 普通 `listVersions` 访客 | 不可见 | 不可见 | 可见 | 不可见 | 不可见 | -| `listVersions` 的 owner / namespace ADMIN / OWNER | 可见 | 可见 | 可见 | 可见 | 可见 | -| 常规 `getVersionDetail` | 不可读 | 仅 owner 可读 | 可读 | 不可读 | 不可读 | -| 下载 / resolve / tag / 文件读取 | 不可用 | 不可用 | 可用 | 不可用 | 不可用 | -| review 详情页 | 可见完整快照 | 可见完整快照 | 可见完整快照 | 可见完整快照 | 可见完整快照 | - -补充: -- `YANKED` 版本仍出现在管理视角的版本列表中,但不可下载 -- `yank` 当前最新已发布版本时,会重算 `latest_version_id` 指向下一个最新的 `PUBLISHED` 版本;若没有,则置空 - -#### 1.3.3 审核 / 推广 / 治理动作 - -| 角色 | 发布新版本 | 提交审核 | 审核团队空间 | 审核全局空间 | 提交推广 | 审核推广 | hide / unhide | yank 已发布版本 | -|------|------------|----------|--------------|--------------|----------|----------|---------------|----------------| -| 匿名用户 | 不可 | 不可 | 不可 | 不可 | 不可 | 不可 | 不可 | 不可 | -| namespace MEMBER | 可发布到所属 namespace;新版本进入 `PENDING_REVIEW` | 自己作为 owner 时可;不能代别人提审 | 不可 | 不可 | 自己作为 owner 时可 | 不可 | 不可 | 不可 | -| skill owner | 可 | 可 | 不可 | 不可 | 可 | 不可 | 不可 | 不可 | -| namespace ADMIN / OWNER | 可 | 可为本空间 skill 提交审核 | 可 | 不可 | 可 | 不可 | 不可 | 不可 | -| SKILL_ADMIN | 可提交并可代提审;但普通发布仍非直发 | 可 | 可 | 可 | 可 | 可,但不能审自己的 promotion | 不可 | 可 | -| SUPER_ADMIN | 可跨 namespace 发布且直接 `PUBLISHED`,跳过 membership 检查和 review task | 可 | 可 | 可 | 可 | 可;promotion 和 review 场景下还能审自己的提交 | 可 | 可 | +`YANKED` 可保留在管理版本列表中,但不允许常规下载。yank 命中当前指针时,重算其余已发布版本;不存在时置空。 ### 对象存储写入策略 -一期同步写入正式路径,不使用临时区: -- 文件直接写入 `skills/{skillId}/{versionId}/{filePath}` -- 如果数据库事务失败,对象存储中的文件成为孤儿对象 -- 定时 GC 任务:每天扫描对象存储中存在但数据库中无对应 `skill_file` 记录的文件,清理孤儿对象 -- 删除 DRAFT/REJECTED 版本时,同步清理对应的对象存储文件 +对象直接写入正式单文件路径 `skills/{skillId}/{versionId}/{filePath}` 与整包路径 `packages/{skillId}/{versionId}/bundle.zip`。数据库事务不能自动回滚对象存储: + +- 发布事务注册完成回调;事务未提交时尝试删除本次登记的对象键。清理失败时通过独立事务入口记录补偿;补偿记录本身失败仍可能遗留对象。 +- 版本删除允许 `DRAFT`、`REJECTED`、`SCAN_FAILED`、`UPLOADED`,但不允许通过该接口删除最后一个版本。正常事务路径在数据库提交后删除对象;整条技能硬删除也采用提交后清理,并保护共用对象。 +- 对象删除失败时调用补偿记录入口。当前提交后回调中的普通补偿记录采用默认事务传播;真实事务下的持久化仍需单独验证,不能仅凭回调或 mock 测试宣称补偿可靠落库。 +- 补偿任务针对已记录的失败,每轮完成后间隔 5 分钟重试,单轮最多 100 条;成功标记完成,失败记录尝试。 +- 当前未发现每日扫描整个正式对象存储并与 `skill_file` 对账的通用 GC。进程崩溃或补偿记录失败等情况仍可能遗留对象,不能承诺自动回收所有孤儿。 + +实现依据:`SkillPublishService`、`SkillGovernanceService`、`SkillHardDeleteService`、`SkillStorageDeletionCompensationService` 和 `SkillStorageDeletionCompensationTask`。事务验证要求见[事务与外部副作用](../.trellis/spec/backend/transactions-and-concurrency.md)。 ### CLI publish 请求规范 @@ -134,11 +105,11 @@ Parts: 一期同步响应:服务端同步完成上传、校验、存储、持久化,返回 `200 OK` + skill_version 信息。 -当前 CLI 默认行为:上传 → 进入审核。 -如果调用方持有 `SUPER_ADMIN`,则直接发布为 `PUBLISHED`。 +普通 CLI 上传启用扫描时,响应状态为 `SCANNING`;扫描完成后 PUBLIC/NAMESPACE_ONLY 进入 `PENDING_REVIEW`,PRIVATE 进入 `UPLOADED`。PRIVATE 关闭扫描时直接返回 `UPLOADED`。 +如果调用方持有 `SUPER_ADMIN`,则直接创建 `PUBLISHED` 版本,分发仍受就绪检查约束。 Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确的审核提示。 -`/api/v1/publish` 响应: +`/api/v1/publish` 直发响应示例(普通上传的 status 按上述扫描与可见性分支返回): ```json { @@ -204,25 +175,30 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确 下载请求 │ ▼ -① 校验技能状态(ACTIVE)、版本状态(PUBLISHED) +① 校验技能状态(ACTIVE)与目标版本 + - PUBLISHED:须 downloadReady 且未 yank + - UPLOADED/PENDING_REVIEW:仅 owner 或 namespace ADMIN/OWNER │ ▼ -② 可见性检查 - - PUBLIC: 任何人(包括匿名用户) - - NAMESPACE_ONLY: 该 namespace 的成员(需登录) - - PRIVATE: owner 本人 + 该 namespace 的 ADMIN 以上(需登录) +② 可见性及 namespace 状态检查 + - PUBLIC、NAMESPACE_ONLY、PRIVATE、hidden 按对应授权规则处理 + - 不能因版本可读就跳过容器授权 │ ▼ -③ 返回预生成包或按文件清单打包 +③ 构建下载结果:优先预生成整包,缺失时按可读取文件重新打包 │ ▼ -④ 审计与统计 - - audit_log 同步写入(记录下载人/IP/版本) - - download_count 异步更新(原子 SQL: download_count = download_count + 1) - - 匿名下载:审计记录 IP + User-Agent,不关联用户 - - 已登录下载:审计记录用户 ID +④ 对 PUBLISHED 同步更新技能和版本下载计数,再发布 SkillDownloadedEvent + - 授权的未发布版本下载不计入公开下载量 + │ + ▼ +⑤ 控制器返回预签名跳转或流式响应 ``` +计数表示请求通过授权并成功构建下载结果,不证明客户端已完整接收。审核绑定下载是独立授权入口,不计入上述公开下载量。缺失 bundle 时的回退会过滤不存在的单文件,因此“生成 ZIP 成功”也不证明历史包完整;完整性保障需要单独验证。 + +下载审计仍是业务要求:登录用户记录身份,匿名用户记录 IP / User-Agent 等必要请求信息。当前已核对的普通 Web / CLI 下载链尚未找到对应的同步审计落库;`SkillDownloadedEvent` 仅携带技能与版本 ID,不能替代该审计。覆盖入口、记录时机与失败行为需要独立确认和验证,本次文档对齐不取消此要求。 + ### download_count 热点行优化预案 一期使用原子 SQL 直接更新,可接受。如出现热点行瓶颈,切换为: @@ -269,9 +245,9 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确 | 事件 | 触发时机 | 消费方 | |------|---------|--------| -| `SkillPublishedEvent` | 审核通过 | 搜索索引写入 | +| `SkillPublishedEvent` | 审核通过或直发等产生已发布版本的路径 | 搜索索引写入 | | `SkillYankedEvent` | 版本撤回 | 搜索索引移除 | -| `SkillDownloadedEvent` | 下载完成 | 下载计数 | +| `SkillDownloadedEvent` | 已发布版本下载结果构建成功并完成同步计数后 | 当前未发现生产消费者;下载计数由下载服务同步完成 | | `SkillStarredEvent` | 收藏/取消 | 收藏计数 | | `SkillRatedEvent` | 评分提交 | 评分重算 | | `ReviewCompletedEvent` | 审核完成 | 预留给后续通知能力(当前可不消费) | @@ -281,7 +257,7 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确 ### 审计日志写入策略 -审计日志统一同步落库,与业务操作在同一请求内同步写入,不走异步事件。审计是企业内部平台的刚性需求,不可容忍丢失。 +审计要求是同步落库,与业务操作在同一请求内记录,不以异步事件替代。审计是企业内部平台的刚性需求;普通 Web / CLI 下载链的已知实现缺口见第 3 节,不能将本要求视为所有入口均已实现的证明。 异步事件仅用于搜索索引、计数器等可容忍延迟的场景。如果后续需要更强一致性,引入 outbox 模式,不依赖 ApplicationEvent + @Async 承担可靠性。 diff --git a/docs/06-api-design.md b/docs/06-api-design.md index a8efd2f9..2d4b420c 100644 --- a/docs/06-api-design.md +++ b/docs/06-api-design.md @@ -208,12 +208,14 @@ Public API 的可见性规则: | 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/skills/{namespace}/{slug}/versions/{version}/withdraw-review` | 撤回提审(PENDING_REVIEW → UPLOADED,同时删除关联的 PENDING review_task) | | POST | `/api/v1/reviews` | 提交指定 `skillVersionId` 进入审核队列 | +| POST | `/api/v1/skills/{namespace}/{slug}/submit-review` | 提交 `UPLOADED` 或兼容 `DRAFT` 版本;body 为 `version`、`targetVisibility`(`PUBLIC` / `NAMESPACE_ONLY`) | +| POST | `/api/v1/skills/{namespace}/{slug}/confirm-publish` | 确认发布 PRIVATE 技能的 `UPLOADED` 或兼容 `DRAFT` 版本;body 为 `version` | ### 标签管理 @@ -229,15 +231,15 @@ Public API 的可见性规则: |------|------|------| | 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 版本 | +| DELETE | `/api/v1/skills/{namespace}/{slug}/versions/{version}` | 删除 DRAFT/UPLOADED/REJECTED/SCAN_FAILED 版本;不能删除最后一个版本 | | 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` 预览版本 +- `publishedVersion`:当前最新已发布版本;能否安装仍需通过可见性与安全门禁 +- `ownerPreviewVersion`:owner / namespace 管理者可见、比当前已发布版本更新的未发布版本(排除 `YANKED`);无已发布版本时取最新候选 - `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE` 其中: @@ -246,7 +248,7 @@ Public API 的可见性规则: - owner 详情页在没有可展示发布版本时,才允许 `headlineVersion` 落到 `ownerPreviewVersion` - 推广到全局一律使用 `publishedVersion.id` - `hidden` 是独立治理覆盖层,不属于生命周期状态机 -- 常规版本详情接口只放行 `PUBLISHED`,以及 owner 对自己 `PENDING_REVIEW` 版本的预览;`DRAFT / REJECTED / YANKED` 不通过该接口暴露 +- 常规版本详情与文件预览在技能可见性校验后,允许 owner / namespace `ADMIN` / `OWNER` 查看非发布版本;这不授予安装或下载权限。显式下载另对 `UPLOADED` / `PENDING_REVIEW` 放行有权管理的用户,其他未发布状态不放行。完整边界见 [生命周期文档](./14-skill-lifecycle.md)。 发布成功响应中的 `data` 至少包含以下字段: @@ -260,10 +262,11 @@ Public API 的可见性规则: 发布状态约束: -- 普通用户发布成功后,`status` 为 `PENDING_REVIEW` -- 持有 `SUPER_ADMIN` 的用户通过 Web、`/api/v1/publish`、`/api/v1/publish` 发布时,`status` 为 `PUBLISHED`,且不要求其必须是目标 namespace 成员 -- 当前版本保持该审核策略,不再提供“全员直发”的运行模式 -- 撤回审核不会删除版本记录,而是 `PENDING_REVIEW → DRAFT` +- 普通用户上传 PRIVATE 技能:启用扫描时先进入 `SCANNING`,扫描完成后进入 `UPLOADED`;关闭扫描时直接进入 `UPLOADED`。 +- 普通用户上传 PUBLIC / NAMESPACE_ONLY 技能必须启用扫描,先进入 `SCANNING`,扫描完成后进入 `PENDING_REVIEW`。扫描执行失败与扫描发现风险是不同结果,不能把进入待审等同于通过安全验收。 +- `SUPER_ADMIN` 可免人工审核、免 namespace 成员校验而直接创建 `PUBLISHED` 版本,但不绕过 PUBLIC / NAMESPACE_ONLY 的扫描启用要求,也不表示扫描和安装门禁已通过。 +- 撤回审核保留版本记录,将 `PENDING_REVIEW → UPLOADED`;`DRAFT` 是兼容状态,不是当前撤回目标。 +- 确认私有发布、提交审核、扫描重试及安装门禁见 [生命周期文档](./14-skill-lifecycle.md),不要只根据发布响应的 `status` 判断可安装性。 ## 7.4 Token API(需登录) @@ -278,7 +281,7 @@ Public API 的可见性规则: | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/v1/whoami` | 当前 Bearer Token 对应的用户信息 | -| POST | `/api/v1/publish` | 发布技能包(普通用户进入审核;`SUPER_ADMIN` 始终直发) | +| POST | `/api/v1/publish` | 上传技能包(结果受可见性、扫描和发布权限影响,见发布状态约束) | | GET | `/api/v1/resolve/{namespace}/{slug}` | 解析版本 | | GET | `/api/v1/check/{namespace}/{slug}/{version}` | 本地哈希与远端比对 | @@ -365,16 +368,16 @@ Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN: `latest` 自动跟随最新已发布版本,不可手动移动。 -- `skill.latest_version_id`:每次审核通过自动更新,始终指向最新 PUBLISHED 版本 +- 对外 `latest` 以 `publishedVersion` 表示的最新已发布版本为准;`skill.latest_version_id` 是内部指针,PRIVATE 上传流程可能指向未发布版本,不能单凭该指针判断已发布或可安装。 - `yank` 当前最新已发布版本时,需要同步重算 `latest_version_id` 指向下一个最新的 `PUBLISHED` 版本;若不存在则允许为 `null` -- `latest` 标签:系统保留,只读,自动与 `latest_version_id` 同步 +- `latest` 标签:系统保留,只读,由发布和撤回流程维护;不得将 PRIVATE 上传更新内部指针理解为公开标签也已更新。 - 自定义标签(如 `beta`、`stable-2026q1`):允许人工创建和移动,用于固定安装通道 | 场景 | 使用字段 | 说明 | |------|---------|------| -| 搜索索引内容 | `publishedVersion` / `latest_version_id` | 外部协议仍叫 latest,但内部语义必须等价于最新已发布版本 | -| `/download`(不带版本号) | `publishedVersion` / `latest_version_id` | 下载最新已发布版本 | -| CLI `install @team/skill` | `publishedVersion` / `latest_version_id` | 等同于 `@latest` | +| 搜索索引内容 | `publishedVersion` | 外部协议仍叫 latest,但内部语义必须等价于最新已发布版本 | +| `/download`(不带版本号) | `latest_version_id`,再做下载权限和版本检查 | 公开分发只允许可安装的发布版本;管理者受限下载与公开分发分开判断 | +| CLI `install @team/skill` | `publishedVersion` | 等同于 `@latest` | | CLI `install @team/skill@beta` | `skill_tag` 查询 | 自定义标签指向的版本 | ## 7.9 Resolve 接口说明 @@ -441,7 +444,7 @@ GET /.well-known/clawhub.json | 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` 直发) | +| POST | `/api/v1/publish` | 上传技能(multipart/form-data;状态与安装门禁见发布状态约束) | ### 兼容层请求/响应格式 diff --git a/docs/08-frontend-architecture.md b/docs/08-frontend-architecture.md index 2a018e69..dd573f1b 100644 --- a/docs/08-frontend-architecture.md +++ b/docs/08-frontend-architecture.md @@ -79,7 +79,7 @@ SUPER_ADMIN 可访问所有管理页面。路由守卫检查用户是否持有 - `headlineVersion`:当前页面主展示版本 - `publishedVersion`:当前最新已发布版本 -- `ownerPreviewVersion`:owner / namespace 管理者可见的待审核版本 +- `ownerPreviewVersion`:owner / namespace 管理者可见的新一轮未发布版本;不限于待审核状态,选择规则见[生命周期读模型](14-skill-lifecycle.md#32-lifecycle-projection)。展示投影不等于下载或操作权限。 - `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE` 约束: diff --git a/docs/14-skill-lifecycle.md b/docs/14-skill-lifecycle.md index 2237a368..5b7b48b1 100644 --- a/docs/14-skill-lifecycle.md +++ b/docs/14-skill-lifecycle.md @@ -1,16 +1,16 @@ # Skill Lifecycle -Date: 2026-03-18 -Status: current code-aligned reference +Date: 2026-10-15 +Status: contract reference with documented implementation exception -本文件是 skill 生命周期的单一规范入口。结论以当前代码实现为准,并已经同步到领域模型、业务流程、API、前端、搜索和兼容层文档。 +本文件是 Skill 生命周期的统一导航入口;业务语义结合正式需求、设计与实现测试维护。发现冲突时应明确记录并确认预期,不能自动以代码或旧文档裁定。本次对齐依据见第 8 节;原始 latest 指针的 PRIVATE 例外见第 3.1 节。本页不定义 SuiteVersion 的状态模型。 ## 1. 设计原则 - skill 生命周期不再被建模为一个混杂状态机,而是拆分为容器状态、版本状态、审核工作流状态和可见性覆盖层 - 前端不再从 `status + hidden + latestVersionStatus + viewingVersionStatus` 拼装状态,而统一消费后端 lifecycle projection - destructive action 和 reversible action 必须分离;`withdraw-review` 只表示撤回提审,不表示删除版本 -- 对外仍可保留 `latest` 协议词汇,但内部语义必须严格等价于 latest published +- 对外仍可保留 `latest` 协议词汇,其公共分发语义必须严格等价于 latest published;原始持久化字段的现状另列 ## 2. 状态模型 @@ -26,19 +26,16 @@ Status: current code-aligned reference ### 2.2 SkillVersion 版本状态 -- `DRAFT` -- `PENDING_REVIEW` -- `PUBLISHED` -- `REJECTED` -- `YANKED` - -状态含义: - -- `DRAFT`:可再次提交审核或删除的非公开版本 -- `PENDING_REVIEW`:冻结待审版本 -- `PUBLISHED`:当前可分发版本 -- `REJECTED`:审核拒绝后保留的版本 -- `YANKED`:曾发布、现已撤回分发的版本 +| 状态 | 含义 | +|---|---| +| `DRAFT` | 历史兼容状态,可提审或删除;不作为普通上传的新建状态 | +| `SCANNING` | 等待当前安全扫描完成 | +| `SCAN_FAILED` | 扫描执行失败;不等同于发现风险 | +| `UPLOADED` | 上传保留,等待提审或 PRIVATE 确认发布;不保证已经扫描 | +| `PENDING_REVIEW` | 已提交审核,等待审核决定 | +| `PUBLISHED` | 已发布;实际分发还需满足可见性、容器状态与可安装条件 | +| `REJECTED` | 审核拒绝后保留的版本 | +| `YANKED` | 曾发布、现已撤回分发的版本 | ### 2.3 ReviewTask 审核工作流状态 @@ -52,10 +49,11 @@ Status: current code-aligned reference ### 3.1 Latest -- `Skill.latestVersionId` 的唯一语义是 latest published pointer -- 它只能指向 `PUBLISHED` 版本 -- 若 skill 没有任何已发布版本,则允许为 `null` -- `latest` 系统保留标签自动跟随该指针 +- 公共 `latest`、搜索、兼容接口和默认公开分发必须解析到已发布版本,不能把未发布内容作为公开 latest。 +- 审核通过及自动发布时,`Skill.latestVersionId` 指向发布版本;无已发布版本时允许为空。 +- **已知实现例外:** PRIVATE 上传/重发会将原始 `latestVersionId` 指向新建 `UPLOADED` 版本;启用扫描后该版本可能处于 `SCANNING`。发布测试明确覆盖此指针更新,不能把这个字段单独当作已发布证明。 +- 详情和 owner summary 的 projection 检查指针目标是否 `PUBLISHED`,否则查找已发布版本;批量公开摘要只检查原始指针目标是否可安装,不执行该回退。不能假设各读模型都以相同方式修复指针;`latest` 标签和消费者也不能无条件沿用原始指针。 +- 是否将 PRIVATE 持久化指针也统一为仅指向已发布版本,需要单独设计并检查消费者、历史数据及测试。本次只记录例外,不改变该行为,也不放宽公共 published 约束。 ### 3.2 Lifecycle Projection @@ -63,7 +61,7 @@ Status: current code-aligned reference - `headlineVersion`:当前页面主展示版本 - `publishedVersion`:最新已发布版本 -- `ownerPreviewVersion`:owner / namespace 管理者可见的待审核预览版本 +- `ownerPreviewVersion`:有权限的管理者可见的新一轮未发布工作版本(排除 `PUBLISHED` / `YANKED`,且比已有发布版本更新) - `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE` 约束: @@ -71,15 +69,22 @@ Status: current code-aligned reference - 公开浏览、安装、下载、搜索只认 `publishedVersion` - owner 详情页只有在不存在 `publishedVersion` 时,才允许 `headlineVersion = ownerPreviewVersion` - promotion、compat latest、默认下载等公开分发行为都只能绑定 `publishedVersion` +- 管理者的授权预览下载与公开分发不同:owner 或 namespace ADMIN/OWNER 可下载 `UPLOADED` / `PENDING_REVIEW`,仍须通过 Skill 可见性检查;`DRAFT` 等其他非发布状态不能据此下载。常规 `downloadLatest` 读取原始指针后也使用相同版本授权判断,因此 PRIVATE 指针例外下,默认下载不一定是已发布版本;该行为不授予公众访问权。 +- 已发布版本可安装还要求 `downloadReady=true`、`yankedAt=null`;下载要求容器为 `ACTIVE`,元数据可读不等于可下载 ## 4. 代码实际链路 ### 4.1 首次上传 -- 普通用户上传后直接创建 `PENDING_REVIEW` 版本 -- 同时创建 `PENDING` review task -- 不会创建初始 `DRAFT` -- 不会更新 `latestVersionId` +| 上传路径 | 扫描与后续状态 | 审核任务 / latest | +|---|---|---| +| 普通 `PUBLIC` / `NAMESPACE_ONLY` | 要求扫描启用;创建待审版本,触发扫描进入 `SCANNING`,当前扫描完成后回到 `PENDING_REVIEW` | 创建 `PENDING` review task;不移动 published 指针 | +| 普通 `PRIVATE` | 启用扫描时 `SCANNING -> UPLOADED`;未启用时直接 `UPLOADED` | 不自动创建审核任务;原始指针例外见 3.1 | +| 特权自动发布 | 直接 `PUBLISHED`;启用扫描时仍扫描,但不降为 `SCANNING` | 不创建审核任务;更新发布指针 | + +- 扫描执行失败可进入 `SCAN_FAILED`。扫描完成不代表无风险,风险发现、警告确认和审核门槛是独立检查;过期扫描结果不能完成新一轮扫描。 +- `submit-review` 接受 `UPLOADED`(兼容历史 `DRAFT`),设为 `PENDING_REVIEW` 并创建审核任务,保存请求的目标可见性。 +- `confirm-publish` 仅用于 PRIVATE Skill 的 `UPLOADED`(兼容 `DRAFT`):转为 `PUBLISHED`,更新发布时间与 latest,发出发布事件,不触发新扫描。 ### 4.2 审核通过 @@ -96,16 +101,16 @@ Status: current code-aligned reference ### 4.4 撤回审核 -- `withdraw-review` 的统一语义是 `PENDING_REVIEW -> DRAFT` +- `withdraw-review` 的统一语义是 `PENDING_REVIEW -> UPLOADED` - 同时删除关联的 `PENDING review_task` - 该操作是可逆、非破坏性的 - 当前代码只允许提交人本人撤回 ### 4.5 重传新版本 -- 若发现旧的 `PENDING_REVIEW` 版本,会先把旧版本自动降回 `DRAFT` -- 然后创建新的待审版本 -- 自动撤回与手动撤回必须保持同一语义 +- 若发现旧的 `PENDING_REVIEW` 版本,会先把旧版本自动撤回到 `UPLOADED` +- 然后按可见性及自动发布条件创建新版本,走 4.1 的对应路径 +- 普通交互上传的自动撤回与手动撤回保持同一目标状态;Bundle 发布禁止自动撤回,已有待审版本时应阻塞,由用户先处理 ### 4.6 已发布版本重发 @@ -115,11 +120,11 @@ Status: current code-aligned reference ### 4.7 隐藏 / 恢复 / 归档 / 撤回已发布版本 -- 隐藏:只改 `hidden=true` -- 恢复:只改 `hidden=false` -- 归档:`Skill.status = ARCHIVED` +- 隐藏:设置 `hidden=true` 并记录治理信息,不改变版本状态;不抹除 owner / namespace 管理者的访问权限 +- 恢复:设置 `hidden=false` 并更新治理信息 +- 归档:`Skill.status = ARCHIVED`,保留记录并禁止下载;公开元数据仍可能可读 - 取消归档:`Skill.status = ACTIVE` -- yank:`PUBLISHED -> YANKED` +- yank:`PUBLISHED -> YANKED`,记录撤回人、时间和原因,设置 `downloadReady=false`;没有对应的恢复发布操作 ### 4.8 Yank 后指针修正 @@ -150,12 +155,27 @@ Status: current code-aligned reference ## 6. 权限边界 - `withdraw-review`:仅提交人本人 -- 删除版本:owner 或 namespace 管理者,且仅限 `DRAFT` / `REJECTED` -- 归档 / 取消归档:owner 或 namespace 管理者 -- 隐藏 / 恢复技能、撤回已发布版本:平台技能治理权限 +- 删除版本:owner 或 namespace ADMIN/OWNER,仅限 `DRAFT` / `REJECTED` / `SCAN_FAILED` / `UPLOADED`,禁止删除最后一个版本。数据库删除提交后清理对象存储,失败调用补偿入口;不是跨存储原子删除,补偿持久化的验证限制见[对象存储写入策略](./05-business-flows.md#对象存储写入策略) +- 归档 / 取消归档:owner 或 namespace ADMIN/OWNER +- 隐藏 / 恢复技能:平台技能治理权限 +- yank:owner 或 namespace ADMIN/OWNER 可走普通治理入口;平台治理入口另校验平台权限 +- 提交审核:owner 或 namespace ADMIN/OWNER;PRIVATE 确认发布还支持传入平台角色的 SUPER_ADMIN 路径 +- 读取:可见性检查区分 PUBLIC、NAMESPACE_ONLY、PRIVATE 和 hidden;没有 latest 指针且未隐藏时,普通 namespace 管理角色不自动获得读取权限,owner 可读。接受平台角色的检查入口另支持 SUPER_ADMIN,不等于每个服务调用均传递该角色 +- 写操作授权与读取授权分别校验,不能从管理身份推导所有未发布技能均可读取 ## 7. 当前最终约束 - 一个 skill 生命周期的唯一规范入口就是本文件 - 其它文档如 `02-domain-model`、`05-business-flows`、`06-api-design`、`08-frontend-architecture` 必须与本文件保持一致 -- 若后续代码再次改变生命周期语义,应先修改代码,再同步更新本文件和相关子文档 +- 生命周期行为变化先确认正式需求与验收,再同步修改实现、测试及本文;不能以文档对齐名义静默裁定新业务规则 + +## 8. 依据与验证锚点 + +- 正式语义:[核心语义规则 §6](./oss-02-core-semantic-rules.md#61-状态转换表)、[Bundle 发布规格](../openspec/changes/add-suite-bundle-publishing/specs/suite-bundle-publishing/spec.md);Bundle 不自动撤回,SuiteVersion 使用独立模型。 +- 上传及 PRIVATE 指针:[SkillPublishService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java)、[发布测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java)。 +- 扫描:[SecurityScanService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityScanService.java)、[扫描测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/SecurityScanServiceTest.java)。 +- 提审 / PRIVATE 确认:[SkillReviewSubmitService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillReviewSubmitService.java)、[提审与确认测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillReviewSubmitServiceTest.java)。 +- 撤回 / 删除 / yank:[SkillGovernanceService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java)、[治理测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java)。 +- 读模型与分发:[SkillLifecycleProjectionService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java)、[VisibilityChecker](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/VisibilityChecker.java)、[下载测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadServiceTest.java)、[可见性测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/VisibilityCheckerTest.java)。 + +以上锚点用于核对具体行为,不代表覆盖了所有状态组合,也不等于真实部署或对象存储验收。 From a022279986a1436e95b5398db3870d78a25595f5 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:22:28 +0800 Subject: [PATCH 2/2] docs(lifecycle): resolve deep review contract drift Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- docs/02-domain-model.md | 2 +- docs/04-search-architecture.md | 11 +++++++---- docs/05-business-flows.md | 13 +++++++------ docs/06-api-design.md | 10 +++++----- docs/08-frontend-architecture.md | 8 ++++---- docs/14-skill-lifecycle.md | 15 +++++++++------ 6 files changed, 33 insertions(+), 26 deletions(-) diff --git a/docs/02-domain-model.md b/docs/02-domain-model.md index d6e935f2..47c26007 100644 --- a/docs/02-domain-model.md +++ b/docs/02-domain-model.md @@ -144,7 +144,7 @@ | updated_by | varchar(128) | | | updated_at | datetime | | -- `latest` 是系统保留标签,只读,由发布和撤回流程维护,不允许 API 手动移动;PRIVATE 上传更新内部 `latest_version_id` 不代表已更新公开标签 +- `latest` 是系统保留标签,不允许 API 手动移动。当前标签列表在读取授权后按原始 `latest_version_id` 动态合成,不检查目标版本状态,因此 PRIVATE 上传的未发布指针也可能反映到授权用户的列表中;标签存在不等于可公开分发,下载仍需通过授权、版本状态与就绪检查 - 自定义标签(如 `beta`、`stable-2026q1`)允许人工创建和移动 - 唯一约束:`(skill_id, tag_name)` - `target_version_id` 必须指向 `status = PUBLISHED` 的版本,应用层校验 diff --git a/docs/04-search-architecture.md b/docs/04-search-architecture.md index dd208b1a..32e6c261 100644 --- a/docs/04-search-architecture.md +++ b/docs/04-search-architecture.md @@ -79,10 +79,13 @@ PostgreSQL 全文搜索索引:表增加 `search_vector tsvector` 生成列, ## 4 索引写入时机 -以下场景触发搜索文档更新(upsert by skill_id): -- 审核通过(`PENDING_REVIEW → PUBLISHED`):重算“最新已发布版本”指针,并用该发布版本内容更新搜索文档 -- 已发布版本被撤回(`PUBLISHED → YANKED`):重算“最新已发布版本”指针;若不存在任何已发布版本,则移除搜索文档 -- 技能状态变更(隐藏/归档/恢复):更新搜索文档的 status 字段 +当前 `SearchIndexEventListener` 在事务提交后异步消费以下事件: +- `SkillPublishedEvent`(审核通过、直发等):重建该 skill 的搜索文档。生命周期服务负责更新版本指针,索引监听器不负责重算指针。 +- `SkillVersionYankedEvent`:撤回服务重算发布指针,监听器调用 `rebuildBySkill`。当前重建器即使找不到版本,仍可按 skill 信息生成文档,不会因此直接移除索引行;索引行存在不等于公开可检索。 +- `SkillStatusChangedEvent`:归档时移除搜索文档,取消归档时重建。 +- hide/unhide 当前不发上述状态事件,不靠索引 status 更新生效;查询端关联 skill 并过滤 `hidden`,公开搜索还检查发布状态和下载就绪。 + +实现依据:`SearchIndexEventListener`、`PostgresSearchRebuildService.rebuildBySkill/toDocument` 和 `PostgresFullTextQueryService`。撤回后无发布版本应对公众不可检索,但不能将这一结果写成已物理删除搜索文档。 ## 5 搜索演进路线 diff --git a/docs/05-business-flows.md b/docs/05-business-flows.md index 425f303c..b130eec6 100644 --- a/docs/05-business-flows.md +++ b/docs/05-business-flows.md @@ -54,7 +54,7 @@ 当前代码中的 skill 生命周期展示与操作判断,不再依赖旧的 `latestVersionStatus`、`viewingVersionStatus` 一类拼装字段,而统一基于以下 projection: -- `headlineVersion`:当前详情页/我的技能列表主展示版本 +- `headlineVersion`:projection 的默认主展示候选;前端可按选中版本或管理预览选择展示内容,不保证每个页面都直接显示该字段 - `publishedVersion`:当前已发布版本投影;详情投影与公开列表的就绪过滤存在区别,不能只凭字段非空判定可下载 - `ownerPreviewVersion`:详情 projection 中暴露给 owner / namespace 管理者的新一轮非公开版本;排除 `PUBLISHED`、`YANKED`,按创建时间及 ID 选择最新,且须比已有已发布版本更新 - `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE` @@ -154,7 +154,7 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确 - 全局空间的新 skill 由其 owner 独立管理版本 - 原团队 skill 可继续独立迭代 - 两者版本不自动同步。来源 owner 或 namespace 管理员可在团队详情选择已发布版本,再次提交平台提升审核;审核通过后在原全局 skill 上新增版本,不覆盖原有版本 -- 再次提升通过后,全局 skill 的展示名和摘要也更新为审核时来源 skill 的展示信息,使详情页与最新发布内容保持一致 +- 首次或再次提升通过时,全局 skill 的展示名和摘要优先取所选来源版本的 `parsedMetadataJson`(`displayName` / `description`);该字段为空时回退到审核时来源 skill 的展示信息。非空元数据解析失败会抛错,不会静默回退 - 申请和审批时均检查目标身份、来源版本状态及全局同号冲突。全局独立上传可以同时待审;不同号版本分别发布,后通过者成为 latest,同号后通过的申请失败 - 版本号不比较数值高低;例如全局当前 v1.3、团队提交尚未占用的 v1.1,确认提示后仍可提交,审批通过后 v1.1 成为 latest,v1.3 保留在历史 @@ -166,7 +166,7 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确 提升流程当前严格绑定已发布版本: -- promotion request 的 `source_version_id` 必须指向 `publishedVersion.id` +- promotion request 的 `source_version_id` 必须属于来源 skill 且状态为 `PUBLISHED`;可以选择历史已发布版本,不要求等于 projection 的 `publishedVersion.id` - 不允许直接提升 `ownerPreviewVersion` ## 3 下载流程 @@ -246,12 +246,13 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确 | 事件 | 触发时机 | 消费方 | |------|---------|--------| | `SkillPublishedEvent` | 审核通过或直发等产生已发布版本的路径 | 搜索索引写入 | -| `SkillYankedEvent` | 版本撤回 | 搜索索引移除 | +| `SkillVersionYankedEvent` | 已发布版本撤回 | 提交后异步重建该 skill 的搜索文档;无剩余发布版本不代表物理移除索引行,见[索引写入时机](./04-search-architecture.md#4-索引写入时机) | +| `SkillStatusChangedEvent` | 归档 / 取消归档 | 归档移除搜索文档,取消归档重建;hide/unhide 不走此事件 | | `SkillDownloadedEvent` | 已发布版本下载结果构建成功并完成同步计数后 | 当前未发现生产消费者;下载计数由下载服务同步完成 | | `SkillStarredEvent` | 收藏/取消 | 收藏计数 | | `SkillRatedEvent` | 评分提交 | 评分重算 | | `ReviewCompletedEvent` | 审核完成 | 预留给后续通知能力(当前可不消费) | -| `SkillPromotedEvent` | 提升到全局 | 搜索索引写入(新 skill) | +| `SkillPublishedEvent`(提升路径) | 首次提升或提升更新审核通过 | 重建目标全局 skill 的搜索文档;不是独立的 `SkillPromotedEvent` | 一期用 Spring ApplicationEvent + `@Async` 实现,后续可替换为消息队列。 @@ -265,7 +266,7 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确 Spring ApplicationEvent + @Async 存在 Pod 被杀时事件丢失的风险。补充以下兜底机制: -- 搜索索引:定时任务每小时检查 `skill_version.status = PUBLISHED` 但 `skill_search_document` 中无对应记录的版本,补建索引 +- 搜索索引:每小时检查已发布 skill 的缺失文档并补建,是待实现的兜底目标;当前已具备重建接口,但未发现对应定时调度,不能作为事件丢失后的自动恢复保证 - 计数器:可接受少量丢失,定时任务每天凌晨从 `skill_star` / `skill_rating` 表重算修正 - 优雅停机:`@Async` 线程池配置 `awaitTerminationSeconds=25`,配合 30s shutdown timeout diff --git a/docs/06-api-design.md b/docs/06-api-design.md index 2d4b420c..16e21738 100644 --- a/docs/06-api-design.md +++ b/docs/06-api-design.md @@ -76,7 +76,7 @@ | 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}/download` | 按原始 latest 指针选择版本,再检查下载权限;公开分发须已发布,管理预览例外见[生命周期读模型](./14-skill-lifecycle.md#32-lifecycle-projection) | | 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` | 按标签下载(解析标签指向的版本后下载) | @@ -237,16 +237,16 @@ Public API 的可见性规则: 当前代码中的 skill 生命周期读模型不再依赖 `latestVersionStatus` / `viewingVersionStatus` 一类拼装字段,而统一使用以下 projection: -- `headlineVersion`:当前页面应展示的主版本 +- `headlineVersion`:projection 的默认主展示候选;前端可按选中版本或管理预览选择展示内容,不保证每个页面都直接显示该字段 - `publishedVersion`:当前最新已发布版本;能否安装仍需通过可见性与安全门禁 - `ownerPreviewVersion`:owner / namespace 管理者可见、比当前已发布版本更新的未发布版本(排除 `YANKED`);无已发布版本时取最新候选 - `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE` 其中: -- 公开详情、公开安装、公开搜索一律只认 `publishedVersion` +- 默认公开版本展示、安装与搜索以最新已发布版本为候选;显式版本和自定义标签可选择其他 `PUBLISHED` 版本,仍须通过对应入口的授权与就绪检查 - owner 详情页在没有可展示发布版本时,才允许 `headlineVersion` 落到 `ownerPreviewVersion` -- 推广到全局一律使用 `publishedVersion.id` +- 提升到全局可选择属于来源 skill 的任一 `PUBLISHED` 版本,包括历史版本;不要求等于 `publishedVersion.id` - `hidden` 是独立治理覆盖层,不属于生命周期状态机 - 常规版本详情与文件预览在技能可见性校验后,允许 owner / namespace `ADMIN` / `OWNER` 查看非发布版本;这不授予安装或下载权限。显式下载另对 `UPLOADED` / `PENDING_REVIEW` 放行有权管理的用户,其他未发布状态不放行。完整边界见 [生命周期文档](./14-skill-lifecycle.md)。 @@ -370,7 +370,7 @@ Admin API 按最小权限拆分,不再统一要求 SUPER_ADMIN: - 对外 `latest` 以 `publishedVersion` 表示的最新已发布版本为准;`skill.latest_version_id` 是内部指针,PRIVATE 上传流程可能指向未发布版本,不能单凭该指针判断已发布或可安装。 - `yank` 当前最新已发布版本时,需要同步重算 `latest_version_id` 指向下一个最新的 `PUBLISHED` 版本;若不存在则允许为 `null` -- `latest` 标签:系统保留,只读,由发布和撤回流程维护;不得将 PRIVATE 上传更新内部指针理解为公开标签也已更新。 +- `latest` 标签是系统保留、API 不可手动移动的标签。当前 `SkillTagService.listTags` 通过读取授权后,直接按原始 `latest_version_id` 合成该标签,不检查目标版本状态;PRIVATE 未发布指针也可能出现在授权用户的标签列表中。这是列表现状,不代表该版本可以公开下载,解析与下载仍须执行各自门禁。 - 自定义标签(如 `beta`、`stable-2026q1`):允许人工创建和移动,用于固定安装通道 | 场景 | 使用字段 | 说明 | diff --git a/docs/08-frontend-architecture.md b/docs/08-frontend-architecture.md index dd573f1b..4524c930 100644 --- a/docs/08-frontend-architecture.md +++ b/docs/08-frontend-architecture.md @@ -34,7 +34,7 @@ | 技能详情页 | `/@{namespace}/{slug}` | README 渲染、版本、评分、收藏、下载 | | 版本历史 | `/@{namespace}/{slug}/versions` | 版本列表 + changelog | -门户区所有 PUBLIC 技能匿名可浏览和下载,无需登录。 +门户区对符合读取条件的 PUBLIC 技能开放匿名浏览;PUBLIC 本身不保证可下载。隐藏及可见性检查、下载时容器须为 ACTIVE、目标版本的发布和可安装门禁仍然生效,归档技能的元数据可读不代表可下载。 ### 2.2 个人中心(需登录) @@ -77,15 +77,15 @@ SUPER_ADMIN 可访问所有管理页面。路由守卫检查用户是否持有 前端不再从 `status + hidden + latestVersionStatus + viewingVersionStatus` 拼装 skill 生命周期,而统一消费后端返回的 projection: -- `headlineVersion`:当前页面主展示版本 +- `headlineVersion`:projection 的默认主展示候选;前端可按选中版本或管理预览选择展示内容,不保证每个页面都直接显示该字段 - `publishedVersion`:当前最新已发布版本 - `ownerPreviewVersion`:owner / namespace 管理者可见的新一轮未发布版本;不限于待审核状态,选择规则见[生命周期读模型](14-skill-lifecycle.md#32-lifecycle-projection)。展示投影不等于下载或操作权限。 - `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE` 约束: -- 详情页和“我的技能”列表统一以 `headlineVersion` 作为主展示版本 -- 安装、下载、promotion 等公开分发相关操作只允许绑定 `publishedVersion` +- 详情页根据选中版本展示对应信息;“我的技能”卡片在有不同于已发布版本且非 YANKED 的 `ownerPreviewVersion` 时,优先显示其标题、摘要与状态,点击进入该预览版本。标题/摘要缺省时回退到 skill 展示信息,状态再回退到 `headlineVersion.status` / skill 状态;隐藏筛选或归档/隐藏容器状态优先覆盖版本状态,隐藏筛选下卡片不跳转 +- 默认安装/下载以最新已发布版本为候选;显式版本或自定义标签可指向其他已发布版本。promotion 可选择属于来源 skill 的任一 `PUBLISHED` 版本,不限于 `publishedVersion`;各操作仍须通过对应授权与就绪检查 - `hidden` 是独立治理覆盖层,不属于版本生命周期状态机 ## 4 登录与鉴权 diff --git a/docs/14-skill-lifecycle.md b/docs/14-skill-lifecycle.md index 1e168f27..527e43d8 100644 --- a/docs/14-skill-lifecycle.md +++ b/docs/14-skill-lifecycle.md @@ -59,16 +59,16 @@ Status: contract reference with documented implementation exception 详情页、我的技能、我的收藏、搜索等读模型统一基于以下 projection: -- `headlineVersion`:当前页面主展示版本 +- `headlineVersion`:projection 的默认主展示候选;前端可按选中版本或管理预览选择展示内容,不保证每个页面都直接显示该字段 - `publishedVersion`:最新已发布版本 - `ownerPreviewVersion`:有权限的管理者可见的新一轮未发布工作版本(排除 `PUBLISHED` / `YANKED`,且比已有发布版本更新) - `resolutionMode`:`PUBLISHED` / `OWNER_PREVIEW` / `NONE` 约束: -- 公开浏览、安装、下载、搜索只认 `publishedVersion` +- 默认公开展示、安装与搜索以最新已发布版本为候选;显式版本或自定义标签可指向其他 `PUBLISHED` 版本,仍须通过入口授权与就绪检查 - owner 详情页只有在不存在 `publishedVersion` 时,才允许 `headlineVersion = ownerPreviewVersion` -- promotion、compat latest、默认下载等公开分发行为都只能绑定 `publishedVersion` +- compat latest 和默认公开下载采用最新已发布版本语义;promotion 可选择来源 skill 的任一 `PUBLISHED` 版本,不要求等于 `publishedVersion`。原始指针与各入口解析差异见第 3.1 节 - 管理者的授权预览下载与公开分发不同:owner 或 namespace ADMIN/OWNER 可下载 `UPLOADED` / `PENDING_REVIEW`,仍须通过 Skill 可见性检查;`DRAFT` 等其他非发布状态不能据此下载。常规 `downloadLatest` 读取原始指针后也使用相同版本授权判断,因此 PRIVATE 指针例外下,默认下载不一定是已发布版本;该行为不授予公众访问权。 - 已发布版本可安装还要求 `downloadReady=true`、`yankedAt=null`;下载要求容器为 `ACTIVE`,元数据可读不等于可下载 @@ -115,8 +115,9 @@ Status: contract reference with documented implementation exception ### 4.6 已发布版本重发 - rerelease 当前本质上是从已发布版本复制并重新走发布流程 -- 当前实现允许特权路径直接产出新 `PUBLISHED` 版本 -- 该能力应被理解为发布路径特例,不是生命周期展示态 +- 入口先检查生命周期管理权限,要求来源版本为 `PUBLISHED`、目标版本号未占用;内部传空平台角色集且 `forceAutoPublish=false`,不沿用平台特权直发 +- 新版本按原 skill 的可见性及扫描配置进入 4.1 对应的上传、扫描或待审路径;内部绕过重复成员校验不等于绕过审核 +- 该能力是复制内容重新发布,不是生命周期展示态 ### 4.7 隐藏 / 恢复 / 归档 / 撤回已发布版本 @@ -148,7 +149,8 @@ Status: contract reference with documented implementation exception ### 5.3 提升更新与撤销 -- 提升先要求团队来源版本 `PUBLISHED`,再单独进入平台提升审核;后续更新同一全局 Skill 时为它新增不可变的已发布版本。 +- 提升要求所选版本属于团队来源 Skill 且为 `PUBLISHED`,可选历史版本,再单独进入平台提升审核;后续更新同一全局 Skill 时为它新增不可变的已发布版本。 +- 首次和后续提升均优先从所选来源版本的 `parsedMetadataJson` 读取展示名和摘要;字段为空才回退到审核时来源 Skill 的展示信息,非空但解析失败则抛错。 - 全局 Skill 也可独立上传并审核。提升更新与独立上传使用相同版本号时不能互相覆盖;不同号版本分别发布,latest 指向最后通过审核的版本,不按版本字符串大小排序。 - 撤销提升不同于归档或隐藏:平台审核通过后移除全局派生 Skill 的公众入口及记录,保留来源团队 Skill、提升和撤销审计,并保护共用对象存储文件。重新提升形成新的全局 Skill ID。 @@ -172,6 +174,7 @@ Status: contract reference with documented implementation exception ## 8. 依据与验证锚点 - 正式语义:[核心语义规则 §6](./oss-02-core-semantic-rules.md#61-状态转换表)、[Bundle 发布规格](../openspec/changes/add-suite-bundle-publishing/specs/suite-bundle-publishing/spec.md);Bundle 不自动撤回,SuiteVersion 使用独立模型。 +- 提升展示信息:[版本展示名规格](../openspec/changes/refine-display-name-publishing/specs/skill-display-name/spec.md)要求标题与摘要匹配所选版本快照,取代旧提升方案中读取来源 Skill 当前展示信息的描述;[PromotionService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java)保留空元数据的兼容回退。 - 上传及 PRIVATE 指针:[SkillPublishService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java)、[发布测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java)。 - 扫描:[SecurityScanService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/security/SecurityScanService.java)、[扫描测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/security/SecurityScanServiceTest.java)。 - 提审 / PRIVATE 确认:[SkillReviewSubmitService](../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillReviewSubmitService.java)、[提审与确认测试](../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillReviewSubmitServiceTest.java)。