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

21 KiB
Raw Blame History

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。首版结构固定为:

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)显式开启。