skillhub/openspec/changes/add-suite-bundle-publishing/design.md

305 lines
21 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.

## Context
需求背景与范围见 `proposal.md`。当前 Web 可以手工创建 Suite、编辑 DRAFT,并基于已有快照创建
新 SuiteVersion,但不能一次提交多个本地 Skill 文件夹、自动比较内容并统一跟踪成员发布。当前
Suite 使用不可变 SuiteVersion 快照,成员必须是精确的
PUBLISHED SkillVersion,并允许引用符合可见性要求的跨 Namespace PUBLIC Skill。普通 Skill 发布
可能涉及对象存储、异步扫描和独立人工审核。技能市场已经使用 `/search`,套件专区已经使用
`/suites`、类型化资源接口和 Suite 卡片。Skill 详情目前只有在该 Skill 是 Entry Skill 时才展示
所属 Suite。Suite 的 summary 和 overview 当前可为空,因此已发布 Suite 可能只能显示空内容提示。
现有 Label 用于 Skill 容器分类,Tag 是后端版本别名。
## Goals / Non-Goals
**目标:**
- 将一次 ZIP 或根目录上传转换为稳定、可检查的 Suite 创建或更新计划。
- 允许在操作者具备 Skill 创建权限的 Namespace 中,从携带包创建新 Skill。
- 允许继续精确引用合规的非本人所有 Skill,且不尝试发布它们。
- 只发布操作者已经具备独立发布权限的携带包成员。
- 扫描和审核期间不创建临时或部分有效的 SuiteVersion。
- 让重试、异步审核、权限变化和终止失败都可观察。
- 在任意成员 Skill 上安全展示当前用户可见的所属 Suite。
- 让 Suite 拥有自己的 Label,并要求新发布版本具备有效摘要和概述。
- 限制归档处理、数据库查询、扫描和反向引用投影的资源消耗。
**非目标:**
- 为操作者只能引用、不能管理的 Skill 发布新内容。
- 替代单 Skill 的独立扫描/审核或 Suite 自身审核。
- 合并技能市场和套件专区,或者改变两个入口的路由与产品定位。
- 批量修改成员 Label、增加 Suite Tag、迁移 Namespace、按成员关键词参与排名、CLI 上传
Bundle、嵌套 Suite 或跨 Registry 成员。
## Decisions
### 控制台套件清单:采用方案 2(2026-09-16 用户确认)
移除“我的套件”的独立发布任务页签。套件、尚未形成套件的创建操作统一服务端分页;按坐标归并,
生成套件后不重复列出成功操作。顶部采用 288px 搜索框、状态下拉和轻量“需处理”数量提示。
数量针对当前搜索所有页,而非本页;筛选、分页和计数在数据库中完成。新工作台接口保留旧列表接口
兼容性,每次固定两条 SQL,不读取成员、执行计划 JSON 或包内容;每页 12 条,搜索只在点击搜索
或按 Enter 后执行,输入与已提交条件分离,取消过时请求,仅存在运行/等待操作时每 5 秒轮询。正式套件版本审核复用现有“我的审核进度”。
### 1. 创建和更新共用 Manifest,成员分为“携带包”和“精确引用”
Manifest 描述完整目标成员集合和顺序,每个成员只能选择一种形式:
- **携带包成员**:指向一个以 `SKILL.md` 为根的包目录。Skill 不存在时,操作者必须在目标
Namespace 具备创建权限;Skill 已存在时,操作者必须具备该 Skill 的发布权限。通过规范化包
fingerprint 判断创建 Skill、创建 SkillVersion,还是复用现有精确版本。
- **精确引用成员**:指向一个已有、精确、PUBLISHED 的 SkillVersion,不携带包,Bundle 不修改它。
操作者只需满足现有 Suite 编排和读取权限,不需要拥有该 Skill。
创建模式没有基准快照,全部成员均为新增。更新模式以明确的 SuiteVersion 为基准,只有基准成员
未出现在 Manifest 中时才视为移除,不能因为没有包目录就视为移除。引用成员可以新增,也可以
显式重新固定到另一个合规 PUBLISHED 版本。Entry Skill 可以使用任一成员形式。
实现协议使用归档根级唯一文件 `SUITE.yaml`。首版结构固定为:
```yaml
apiVersion: skillhub.iflytek.com/v1alpha1
kind: SkillSuiteBundle
metadata:
namespace: global
slug: clinical-workflow
spec:
mode: CREATE # 或 UPDATE
baseVersion: 1.0.0 # 仅 UPDATE 必填,CREATE 禁止提供
version: 1.1.0
displayName: 临床工作流
summary: 套件摘要
overview: 套件 Markdown 概述
visibility: PUBLIC
changelog: 本次更新说明
entry: "@global/intake"
members:
- skill: "@global/intake"
package:
path: skills/intake
visibility: PUBLIC # 仅新 Skill 必填
- skill: "@global/shared-dictionary"
reference:
version: 2.3.1
```
协议采用严格字段集合,不识别的字段直接报错,避免拼写错误被静默忽略。`metadata` 坐标和
`spec.version` 是目标 Suite 身份;成员 `skill` 是唯一 Skill 身份;`SKILL.md` 的 `name`、
`description` 和可选 `version` 继续作为 Skill 自身发布元数据。若携带包的 `SKILL.md` 解析出的
slug 与成员 `skill` 的 slug 不同,或显式版本与预览解析出的目标版本冲突,预览阻塞,不设置覆盖
优先级。引用成员只能提供精确 `version`,不能同时携带包。已有 Skill 的 `package.visibility`
可以省略并继承当前值;若显式提供则必须与当前值一致。新 Skill 必须明确提供该字段。
**备选方案:要求每个成员都有包目录。** 不采用。Suite 可以合法引用其他用户的公开 Skill,强制
打包会错误暗示操作者可以重新发布或复制这些内容。
### 2. Suite 创建和更新使用同一个持久化 Saga
预览阶段保存有期限且归属当前操作者的 `PreviewSession`、临时归档和完整计划,但不占用 Suite 坐标
或版本。`PREVIEW_READY` 是 PreviewSession 状态,不是执行操作状态。确认事务先创建持久化
`ExecutionOperation` 并原子获取目标 Suite 坐标或版本占用;获取失败时必须在任何成员生命周期
副作用前结束,并要求重新预览。
ExecutionOperation 保存创建/更新模式、目标 Namespace/Suite/版本、操作者、归档摘要、完整成员计划、
已创建 Skill/SkillVersion ID、精确引用 ID 和失败原因。对外状态为 `RUNNING`、
`WAITING_FOR_MEMBERS`、`BLOCKED_RETRYABLE`、`REPREVIEW_REQUIRED`、`SUITE_DRAFT_CREATED` 和
`CANCELLED`。只有 PreviewSession 使用 `PREVIEW_READY`、`CONFIRMED` 和 `EXPIRED`;等待人工审核的
ExecutionOperation 不受 PreviewSession TTL 影响。
只有需要创建或发生变化且有权限的携带包成员进入现有 Skill 发布流程。所有新版本成为 PUBLISHED,
并且复用成员和引用成员最终仍符合要求后,更新模式才创建 SuiteVersion DRAFT;创建模式原子创建
Suite 容器及首个 DRAFT。任一成员失败都会阻止生成 Suite 草稿,但不会生成部分 SuiteVersion。
领域事件触发状态协调,同时使用有界定时恢复处理事件丢失或乱序。
**备选方案:SuiteVersion 暂时引用未发布成员。** 不采用。这会破坏 #828 的精确 PUBLISHED
版本不变量,并把临时成员处理扩散到 Suite 校验、审核、详情、删除和安装全链路。
### 3. 文件只提交一次,先预览再确认
Web 接受一个 ZIP;支持目录选择的浏览器还可以选择一个根目录,并以同一归档协议提交。服务端将
归档流式写入临时对象存储,校验 Manifest 和携带包成员,计算 fingerprint,批量读取基准快照、
精确引用和操作者权限。预览为每个成员分别返回成员关系变化 `ADDED`、`UPDATED`、`UNCHANGED`、
`REMOVED`,以及发布动作 `CREATE_SKILL`、`CREATE_VERSION`、`REUSE_VERSION`、`REFERENCE_VERSION`、
`NONE`,避免混淆“加入 Suite”和“创建 Skill”。预览不产生任何生命周期副作用。
确认使用不透明 token,并绑定操作者、模式、目标坐标、归档摘要、目标 Suite 版本和完整计划。
只有确认成功创建的非终态 ExecutionOperation 才独占目标 Suite 坐标或版本。预览阶段解析出的包
版本和引用 ID 在确认与重试过程中保持稳定。创建模式在成员就绪前只通过执行操作占用目标坐标,
不创建对普通用户可见的空 Suite。
### 4. 每个异步边界都保留独立权限
预览明确区分四种权限:
- 在目标 Namespace 创建 Suite,或管理已有 Suite 并创建新版本的权限;
- 在目标 Namespace 创建新 Skill 的权限;
- 读取和编排精确引用版本的权限;
- 发布携带包 Skill 的权限。
服务端在预览、确认、每次创建 Skill/SkillVersion 和 Suite 草稿创建时重新检查对应权限。新 Skill
坐标冲突、目标 Namespace 不可写、失去包发布权限或引用失效都会阻止对应写入或最终创建 Suite
草稿。Suite 所有权不会扩大成员 Skill 权限,错误响应不得泄露无权读取的资源元数据。
### 5. 部分成员发布必须可观察且不可破坏性补偿
确认前尽量发现包、版本、所有权、可见性和策略等确定性错误。但运行期仍可能出现某个成员版本
已经创建、另一个成员失败的情况。重试只处理未完成工作。取消会停止后续编排并阻止创建
SuiteVersion,但不会删除独立创建的 SkillVersion 或已经形成的审核历史。
Bundle 不得调用会自动撤回其他待审版本或删除替换已有版本的发布路径。预览发现同一 Skill 已有
`PENDING_REVIEW` 版本,或目标版本号已经对应任一非 `PUBLISHED` 版本时,直接阻塞并要求用户先在
现有 Skill 流程中处理。这里复用的是普通 Skill 的校验、存储、扫描、审核和审计规则,不是无条件
复用当前具有替换副作用的内存型发布方法。
成员状态收敛规则如下:
| SkillVersion 状态或事件 | Bundle 状态 | 可执行动作 | ID 处理 |
|---|---|---|---|
| `SCANNING`、`PENDING_REVIEW` | `WAITING_FOR_MEMBERS` | 查看扫描或审核进度 | 保持预览绑定 ID |
| PRIVATE 新版本进入 `UPLOADED` | `RUNNING` | 使用 Bundle 确认中已明确授予的私有发布授权,重新鉴权后执行现有 confirm-publish 转换 | 保持 ID |
| `PUBLISHED` | 该成员完成 | 等待其他成员或创建 Suite 草稿 | 保持 ID |
| `SCAN_FAILED` | 能以同一 ID 重扫时为 `BLOCKED_RETRYABLE`,否则为 `REPREVIEW_REQUIRED` | 重扫或重新上传预览 | 不得静默换 ID |
| `REJECTED` | `REPREVIEW_REQUIRED` | 修改内容后重新上传并预览 | 原 ID 不再自动恢复 |
| 待审版本被撤回为 `UPLOADED` | `REPREVIEW_REQUIRED` | 重新预览 | 不自动重新提交审核 |
| 已绑定版本被删除、替换、下架或引用身份变化 | `REPREVIEW_REQUIRED` | 重新预览 | 不跟随新 ID |
| 权限暂时撤销或 Namespace 冻结且计划身份未变化 | `BLOCKED_RETRYABLE` | 恢复权限或状态后重试,也可由有权角色取消 | 保持 ID |
`BLOCKED_RETRYABLE` 保留坐标/版本占用,只允许同一 ExecutionOperation 按原计划和原 ID 重试。
`REPREVIEW_REQUIRED` 是终态,进入时与坐标/版本占用在同一事务中释放。`CANCELLED` 和
`SUITE_DRAFT_CREATED` 同样在终态事务中释放占用。数据库使用仅覆盖占用状态的唯一约束,防止释放
与新确认并发时出现双写。等待审核和可重试阻塞不会因 PreviewSession 过期而自动释放;原操作者或
当前治理角色可以按权限取消长期操作。
状态读取只允许原操作者或当前有权治理目标 Suite/Namespace 的角色。读取时仍按当前权限过滤成员
元数据;重试和取消必须重新授权。最终创建 Suite 草稿前再次检查 Namespace 可写、Suite 创建或
管理权限、已有 Suite 仍为 ACTIVE、目标坐标/版本占用和全部成员资格。权限或 Namespace 状态暂时
不可用且计划身份未变化时进入 `BLOCKED_RETRYABLE`;资源身份或目标计划失效时进入
`REPREVIEW_REQUIRED`。
新 Skill 的目标可见性必须由 Manifest 明确提供。已有 Skill 始终继承当前 Skill 可见性,本功能不
承担可见性迁移;Manifest 提供不同值时阻止确认。预览展示每个携带包成员的最终可见性以及审核或
PRIVATE 直接发布路径,并在确认、成员写入和 Suite 草稿创建前使用现有受众兼容规则重新检查。
预览还保存每个成员当前 warning 集合及其摘要。确认页逐成员展示 warning,用户必须对 warning
执行独立于通用 Bundle 确认的明确确认;确认请求绑定已展示的 warning 摘要。warning 变化会使
预览失效,服务端不得因为用户只点击通用确认就静默启用普通发布的 `confirmWarnings`。
### 6. 保持现有 Skill fingerprint 和 Suite 快照模型
每个包目录都规范化为以自身 `SKILL.md` 为根的普通 Skill 包,并使用现有校验器和 fingerprint
算法。ZIP 文件顺序、压缩元数据、压缩级别和外层目录不参与内容身份。纯引用成员只比较精确
SkillVersion 身份和有效性,不下载或计算其包内容。
Manifest 提供目标 Suite/SuiteVersion 的展示信息、完整顺序和 Entry Skill。新建和变化包继续
使用现有发布版本规则;自动解析的版本在预览时固定。最终结果仍是一个只包含精确 PUBLISHED
引用的普通 DRAFT SuiteVersion。
### 7. 限制资源成本并保证归档安全
归档采用流式解析,拒绝危险路径、链接、重复规范化名称、过度解压以及超过配置的归档/成员/文件
限制。预览把成员文件保存为临时对象定位信息、大小、内容类型和已计算摘要,而不是把最大 Bundle
展开为一组常驻内存的 `byte[]`。确认后的发布边界从暂存对象流式读取并复用预览摘要,不重新解压
或计算 fingerprint;普通发布需要的文件 hash、包归档和存储对象也从该流式输入生成。当前成员、
引用、版本和权限批量读取。纯引用和未变化成员不复制对象、不扫描、不审核。
首版复用 `skillhub.publish.max-package-size` 同时限制 ZIP 压缩体积和整个 Bundle 的解压后总量,
单文件限制继续复用 `skillhub.publish.max-single-file-size`;Bundle 总文件数上限为普通单 Skill 文件数
上限乘以协议允许的 100 个成员。上传流先写入独立临时目录,每个 ZIP 文件只在解压时计算一次
SHA-256 并写入临时对象,分析阶段只读取本地暂存文件,返回计划不保留文件字节。
外层解析和成员解析分开执行。外层只负责唯一 Manifest、成员目录边界和文件归属;成员目录去除
自身前缀后,必须成为一个普通的、根部含 `SKILL.md` 的 Skill 包,并复用现有
`SkillPackageValidator`、元数据解析、合规规则和错误/警告等级。成员目录不得重叠或嵌套,未声明的
`SKILL.md` 和无法归属的普通文件均阻止确认,避免服务端猜测用户意图。Manifest 与 `SKILL.md`
重复字段的优先级必须在协议中唯一确定;必须一致的字段发生冲突时直接报错。
格式校验、fingerprint 和权限分析都发生在预览阶段;Scanner 仍属于确认后的普通 Skill 生命周期,
不为了预览创建扫描任务。任一成员存在阻塞性格式错误时,页面可以展示全部已发现问题,但整个
Bundle 不可确认。
协调任务依靠带索引的操作/成员状态,不重新读取归档。反向引用使用集合式、有上限或分页的查询,
不得逐条查询 Suite。
### 8. 技能市场与套件专区保持分开
技能市场不增加类型切换。`/search` 继续只展示 Skill,并保留 Skill Label、收藏和排序。
`/suites` 继续作为 Suite 专区,使用类型化资源接口和现有 Suite 卡片。
唯一新增的发现能力是反向成员关系:Skill 详情查询最新、ACTIVE、非隐藏、PUBLISHED 且包含当前
Skill 的 SuiteVersion,不再要求当前 Skill 必须是 Entry。结果标明 Entry 身份,只返回用户有权
读取的兄弟成员摘要;无权读取的成员仅显示数量。成员关键词暂不影响 Suite 排名。
### 9. Suite 拥有自己的 Label,但不向成员级联
复用 `LabelDefinition`、本地化、`visibleInFilter`、排序以及 NORMAL/PRIVILEGED 权限语义,但使用
独立的 Suite-to-Label 关联。Suite Label 用于 Suite 容器展示和套件专区筛选。增删 Label 不创建
SuiteVersion,也不修改任何成员 Skill Label。
**备选方案:把现有 Skill Label 关联迁移为通用多态资源表。** 暂不采用。这会迁移稳定的 Skill
数据并削弱数据库外键约束,当前收益不足。独立关联可以复用 Label 定义,同时保持所有权清晰。
Tag 继续属于 SkillVersion。SuiteVersion 已经有显式版本,因此本次不增加 Suite Tag,也不向
成员批量设置 Tag。
### 10. 在发布边界要求有效摘要和概述
DRAFT SuiteVersion 可以在 summary 或 overview 尚未完成时保存。提交审核、PRIVATE 直接发布和
最终批准时,两者都必须非空。summary 是套件专区、卡片和引用使用的简短说明;overview 是
Markdown,说明用途、成员职责或顺序、预期输入输出和使用边界。服务端负责强制校验,不自动写入
“精选技能组合”之类的泛化文案。
已有字段为空的 PUBLISHED SuiteVersion 仍可读取和安装,避免破坏历史数据;创建下一个版本时
必须补齐。Bundle 可以显式提供新值,也可以继承当前 SuiteVersion 的非空值,预览必须展示最终
解析结果。
### 11. Suite 概述和 Entry Skill 说明各自承担职责
Suite overview 说明组合用途、适用场景、成员分工或调用顺序、预期输入输出和使用边界。编辑器提供
这些章节的结构化模板和完整性提示,但不以机械字数门槛鼓励填充内容,也不自动拿成员文档冒充
Suite 作者的概述。
Suite 详情可以在独立折叠区域按需展示 Entry Skill 固定 SkillVersion 的 `SKILL.md`。内容必须延迟
加载、复用该精确版本现有读取权限和安全 Markdown 渲染,并明确标注来源坐标与版本。Entry Skill
说明不能替代 Suite overview;无权访问、版本失效或加载失败时,只保留合规的成员状态或跳转入口。
### 12. Web 将导入作为现有创建新版本流程的一种方式
“创建 Suite”和“创建新版本”均先选择“从技能市场组合”或“从本地导入”。手工组合保留现有页面
和 API;本地导入进入上传、差异预览、明确确认和操作进度。更新预览必须突出显示因未出现在完整
Manifest 中而被移除的成员。确认文案列出将创建的 Skill/SkillVersion 数量、复用和引用数量、
移除数量以及最终只创建 Suite 草稿这一结果。
进度页逐成员展示创建、扫描、审核、复用、引用、失败和阻塞状态,并提供允许范围内的重试、取消、
审核详情和最终草稿入口。页面刷新或重新登录后可以依靠操作 ID 恢复进度,不依赖仍保留在浏览器
内存中的文件。
## Risks / Trade-offs
- **引用的公开 Skill 后续可能失效** → 创建 Suite 草稿前重新校验;发布后继续使用现有 degraded
机制。
- **Bundle 阻塞后可能留下独立成员版本** → 展示每个成员结果;重试和取消不得破坏成员生命周期。
- **事件可能重复、丢失或乱序** → 使用幂等协调和有界恢复任务。
- **预览后权限可能变化** → 每个异步边界重新检查对应权限,失败时阻止而不是放宽权限。
- **并发创建可能争用坐标,更新可能争用版本** → 操作记录独占目标 Suite 坐标和版本,最终创建
草稿时重新检查唯一性并保留乐观锁。
- **创建模式的某些 Skill 已发布、Suite 最终失败** → 独立 Skill 生命周期不回滚;进度页明确展示,
重试只继续未完成工作。
- **恶意归档可能消耗 CPU 或内存** → 流式处理、限制解压与文件规模,每个包只计算一次 hash。
- **反向引用可能泄露私有成员构成** → 服务端过滤 Suite 和兄弟成员,只暴露不可访问成员数量。
- **新必填展示信息可能阻塞历史 Suite 更新** → 历史发布快照保持可读,仅在提交新版本时要求补齐。
- **普通作者可能尝试自助配置特权 Label** → 复用现有 Label 权限类型并由服务端执行。
## Migration Plan
1. 增加操作表和索引,不修改现有 Skill/Suite 生命周期表。
2. 先部署预览和状态读取,再启用确认接口与协调任务。
3. 所有实例均能识别操作记录和协调事件后,再开启确认。
4. Bundle 进度、Suite Label/展示信息校验和任意成员反向引用可以独立部署;`/search` 与
`/suites` 路由保持不变。
5. 回滚时关闭新增接口和任务。已有 SkillVersion、SuiteVersion 继续有效;非终态操作保留,供兼容
版本取消或过期处理。
预览默认有效 30 分钟,可通过 `skillhub.suite.bundle.preview-ttl`(环境变量
`SKILLHUB_SUITE_BUNDLE_PREVIEW_TTL`)调整。确认入口默认关闭,待所有实例完成升级后通过
`skillhub.suite.bundle.confirmation-enabled`(环境变量
`SKILLHUB_SUITE_BUNDLE_CONFIRMATION_ENABLED`)显式开启。