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)。