Skip to content
This repository was archived by the owner on Aug 11, 2026. It is now read-only.

Latest commit

 

History

History
549 lines (440 loc) · 29 KB

File metadata and controls

549 lines (440 loc) · 29 KB

Plugin Protocol

@cindy/plugin-protocol 是 plugin-server 与未来 Desktop Plugin 客户端共享的零运行时依赖 TypeScript contract。它不是服务,也不负责运行时协议协商。

使用方式

主仓库通过 cindy-protocol submodule 和 pnpm workspace 引用本包:

{
  "dependencies": {
    "@cindy/plugin-protocol": "workspace:*"
  }
}

所有公开类型、常量和校验器都从包根入口导入:

import {
  PluginProtocolError,
  parseGetPluginResponse,
  parseListPluginsResponse,
  parsePluginMemberUploadStatusResponse,
  parsePluginDownloadResponse,
  validateGhostManifest,
  type GhostManifest,
} from '@cindy/plugin-protocol';

边界

本包只包含:

  • Ghost 包的 ghost.json 类型、格式常量和 validateGhostManifest
  • Desktop 消费的 Plugin 列表、详情与下载响应 DTO、枚举和解析器。
  • organization 成员上传 .cindy 包时由 plugin-server、Cindy Host 和发布者插件共享的 DTO、限制常量和解析器。

本包不包含服务端数据模型、organization 管理 API DTO、Plugin 生命周期实现、受众策略实现、鉴权、对象存储实现、安装目录、启停状态、IPC、panel 布局或其他 Desktop 运行时逻辑。管理面尚无跨仓 TypeScript 消费方,相关类型由 plugin-server 本地维护;未来出现真实共享消费者时再抽取。

校验 Ghost manifest

读取并解析 ghost.json 后,把未知值直接交给校验器。校验器不抛异常,而是返回可判别联合类型:

const rawManifest: unknown = JSON.parse(ghostJsonText);
const result = validateGhostManifest(rawManifest);

if (!result.ok) {
  throw new Error(`ghost.json 不合法: ${result.reason}`);
}

const manifest: GhostManifest = result.manifest;

成功结果是只包含协议已知字段的规范化对象;kind 等有缺省语义的字段会被补齐。不要在校验前把 unknown 强转为 GhostManifest,也不要在服务端或 Desktop 另写一套 manifest 校验规则。

OAuth scope 数量上限

network.secrets[].oauth.scopes 最多包含 256 条;第 257 条会被 validateGhostManifest 拒绝。每条 scope 仍必须是 1–200 字符、不含空白的 唯一字符串,本变更不改变单条校验或重复项规则。

这是 schema v2 的宽松校验变更,不需要提升 manifest 版本。新版 plugin-server 可以发布 49–256 条 scope 的包;仍使用旧协议校验器的 Desktop 会拒绝安装这些包 并保留现有安装,因此应先升级客户端,再分发超过 48 条 scope 的 Plugin。

Cindy 托管网页搜索

插件可通过 cindy.search: ["web"] 请求 Host 提供 Cindy 托管的公网搜索:

{
  "slots": ["tool", "cindy"],
  "tools": [
    {
      "name": "search_web",
      "description": "Search the public web"
    }
  ],
  "cindy": {
    "search": ["web"]
  }
}
  • search 当前只接受动作 web,未知、空或重复动作均拒绝。
  • cindy.search.web 只能由真实 tool-call 触发,因此 manifest 必须同时声明 tool 槽和至少一个工具;只声明 Cindy 能力但没有工具入口会被拒绝。
  • Host 负责路由、凭证与计费,插件不声明或接触网关 Key、模型名和搜索工具定义。

这是 schema v2 的新增严格能力类目。旧版 plugin-server 和 Desktop 会拒绝包含 cindy.search 的包,因此必须先合并本协议、升级两个消费方并部署,再发布使用 该能力的插件。插件 Release 应配合 minCindyVersion,避免旧客户端收到不兼容版本。

Host 托管的内嵌 iOS 模拟器

插件可通过 ios-simulator slot 提供 Host 托管的内嵌 iOS 模拟器入口:

{
  "schemaVersion": 2,
  "id": "simulator-workflow",
  "name": "Simulator Workflow",
  "version": "1.0.0",
  "minCindyVersion": "1.2.3",
  "entry": "main.js",
  "slots": ["ios-simulator"]
}
  • 插件只能读取当前任务的脱敏状态,并请求 Host 打开模拟器控制面板。
  • 视频帧、输入事件、设备标识、Native Helper、WDA、生命周期、恢复与兼容回退 均由 Cindy Host 管理,不会通过本 slot 交给插件。
  • 声明 ios-simulator 时必须同时声明 minCindyVersion。Plugin Server 依照 X-Cindy-Version 选择 Release;版本未知或低于门槛的客户端不会收到该 Release。
  • 本包只登记 manifest 能力,不定义 Cindy Host 内部的 IPC、Sidecar 或媒体协议。

这是 schema v2 的新增严格 slot,不提升 manifest schema 版本。旧版 plugin-server 和 Desktop 会拒绝包含该 slot 的包,因此发布顺序必须是:先合并本协议并升级、部署 Plugin Server 与 Cindy Host,再发布使用该能力的插件。

Manifest 本地化资源

Plugin 可通过可选的 locales 字段声明宿主支持语言对应的包内 JSON 资源:

{
  "locales": {
    "en": "locales/en.json",
    "zh-CN": "locales/zh-CN.json",
    "ja": "locales/ja.json",
    "ko": "locales/ko.json"
  }
}
  • 支持语言固定为 zh-CNenjako;声明 locales 时必须包含 en,供宿主语言不受支持或目标资源缺失时回退。
  • 每条值必须是包内安全相对路径并以 .json 结尾;不同语言不能复用大小写 折叠后相同的路径,也不能与 ghost.json、入口、图标、设置页、面板、Node 入口或 Skill 目录冲突。
  • 单个 locale JSON 的大小上限由 GHOST_LOCALE_MAX_BYTES 固定为 64 KiB。 包文件存在性、UTF-8 JSON 和资源内容由打包、发布及安装侧在读取制品时校验。
  • 这是 schema v2 的可选追加字段,不需要提升 manifest 版本。旧消费方会按未知 字段忽略并继续使用顶层文案;支持该字段的消费方按宿主语言读取资源。

随包渐进披露手册

Plugin 可通过独立顶层字段 manual 声明由 Host 按需读取的 Markdown 手册索引:

{
  "manual": {
    "items": [
      {
        "dir": "manual/getting-started",
        "name": "getting-started",
        "description": "安装、连接与首个任务的完整工作流"
      }
    ]
  }
}
  • manual 不进入 slots,不是运行时能力或权限授权;无手册的既有 Plugin 不受影响。
  • items 必须是 1–8 条。dir 是包内安全相对目录;name 是模型调用 ghost_manual 时使用的逻辑路径首段,沿用小写字母、数字与单连字符分段规则, 最长 64 字符;description 是一级索引说明,长度 1–300 字符。
  • 每个声明目录必须包含固定入口 MANUAL.md。目录树可包含任意深度的子目录;所有 非目录条目都必须是普通 Markdown 文件,且每个文件不超过 64 KiB。文件存在性、 普通文件/非符号链接、严格 UTF-8、大小与二进制拒收由打包、发布及安装侧在读取 制品时校验。不同逻辑 name 的手册单元允许声明祖先/后代目录。
  • manual.items[].dirghost.jsonentryiconsettingsHtmlpanel.htmlnode.entrynode.entries 中声明的文件路径不得相同,也不得存在任一方向的 祖先/后代嵌套关系。Manifest 层会提前拒绝,避免 Markdown-only 手册目录在打包或 装入阶段才因包含已声明的非 Markdown 文件而失败。
  • manual.items[].dir 与任一 locales 文件路径不得相同,也不得存在任一方向的 祖先/后代嵌套关系:Manual 目录不能位于 locale 路径之下,locale 路径也不能位于 Manual 目录之下。Manifest 层会直接拒绝这类声明,避免 Markdown-only 手册目录在 打包或装入阶段才因包含 locale JSON 而失败。
  • 这是 schema v2 的 append-only 可选顶层字段,不需要提升 manifest 版本。旧消费方会 忽略该字段,Plugin 的其它能力继续可用;首个依赖手册才能正确工作的 Release 应声明 minCindyVersion,并在支持 ghost_manual 的 Cindy 发布后再投递。

Node Worker 凭证绑定

声明了 node 槽的插件可以通过 node.secretBindings 请求主机把用户凭证安全持久化,并仅在指定 Worker 入口和 JSON-RPC 方法同时命中时临时注入:

{
  "settingsHtml": "settings.html",
  "slots": ["tool", "node"],
  "node": {
    "entry": "node/worker.cjs",
    "protocol": "json-rpc-stdio",
    "secretBindings": [
      {
        "key": "mail_code",
        "label": "Mail authorization code",
        "methods": ["account/connect", "mail/action"],
        "hint": "Use the provider-generated authorization code",
        "url": "https://mail.example.com/settings"
      }
    ]
  }
}
  • 每个插件最多声明 4 条绑定,每条最多绑定 16 个方法;keynetwork.secretsnetwork.connections 共用命名空间。
  • settingsHtml 必填,负责把凭证一次性写入宿主保险库;浏览器沙箱与 Agent 参数不得接触凭证明文。
  • entry 可省略,省略时仅绑定 node.entry;显式值必须逐字命中 node.entrynode.entries
  • mcp-stdio 绑定不得占用宿主保留的 initializenotifications/initialized 握手方法。
  • url 仅接受不含内嵌用户名或密码的 HTTPS 地址。

这是 schema v2 的可选、追加字段,不改变未声明该字段的现有插件。旧版发布服务器会因严格的 node 字段白名单而拒绝包含该字段的包;因此发布顺序必须是协议仓合并、plugin-server 升级并部署,然后再发布使用该字段的插件。旧版客户端同样会拒绝安装而不会降级为不安全的明文传参。

Host 托管的企业身份凭证

组织插件可以声明 network.secrets[].source: "oidc-token",请求 Cindy Desktop 为当前企业 Membership 按需签发短时 Connection JWT。令牌只在 Host 的 Main 进程内存中使用,插件代码和 Node Worker 都不能读取、保存或转交令牌:

{
  "slots": ["network"],
  "network": {
    "hosts": ["api.example.com"],
    "secrets": [
      {
        "key": "cindy_identity",
        "label": "Cindy organization identity",
        "source": "oidc-token",
        "inject": {
          "header": "Authorization",
          "format": "Bearer {value}",
          "hosts": ["api.example.com"]
        }
      }
    ]
  }
}

该来源必须同时满足:

  • inject.hosts 必须显式非空,并且每项是 network.hosts 中的精确域名;不接受通配符;
  • inject.header 固定为 Authorizationinject.format 固定为 Bearer {value}
  • 不得声明 inputurlexchangeoauth,也不要求 settingsHtml
  • Plugin Server 只允许 scope=organization 的 Release 发布该来源;Public 和 Personal Release 必须拒绝。

这是 schema v2 的新增 manifest 能力,不改变未声明该来源的既有插件。旧版 plugin-server 或 Desktop 不认识该来源时必须拒绝发布/安装,并保留已有安装;部署顺序 应为先合并协议,再升级 plugin-server,最后发布支持正式 Market provenance 的 Desktop。

Host 托管的 GitHub CLI 凭证

受信的 GitHub 插件可以声明 network.secrets[].source: "gh-cli",请求 Cindy Desktop 优先使用本机 gh auth token 的登录令牌;本机未安装 gh、 未登录或读取失败时,回落到用户在该插件设置页保存的备用 Token。 两种令牌都由 Host 选择并注入,插件代码和 Node Worker 无法读取明文:

{
  "settingsHtml": "settings.html",
  "slots": ["network"],
  "network": {
    "hosts": ["api.github.com"],
    "secrets": [
      {
        "key": "github_pat",
        "label": "GitHub login",
        "source": "gh-cli",
        "url": "https://github.com/settings/tokens",
        "inject": {
          "header": "Authorization",
          "format": "Bearer {value}",
          "hosts": ["api.github.com"]
        }
      }
    ]
  }
}

该来源必须同时满足:

  • settingsHtml 必填,作为备用 Token 的写入、替换和清除入口;
  • inject.header 固定为 Authorizationinject.format 固定为 Bearer {value}
  • inject.hosts 必须且只能声明精确域名 api.github.com
  • 不得声明 inputexchangeoauthurl 可用于展示备用 Token 的 HTTPS 申请入口;
  • Plugin Server 与其他 source 一样只消费本 Protocol 的结构校验, 不读取、解析或注入 GitHub 凭证;宿主凭证的信任、用户授权与运行期 注入边界由 Desktop 的统一插件权限模型执行。

这是 schema v2 的新增 manifest 能力,不改变未声明该来源的既有插件。 旧版 plugin-server 或 Desktop 不认识该来源时必须拒绝发布/安装并保留已有 安装。部署顺序为:先合并协议,再升级并部署 plugin-server,然后发布支持 gh-cli 且接入统一插件权限模型的 Desktop,最后上架声明该来源的插件。

解析客户端 HTTP 响应

HTTP 返回体必须先作为 unknown 解析,再交给对应解析器:

const list = parseListPluginsResponse(await listResponse.json());
const detail = parseGetPluginResponse(await detailResponse.json());
const download = parsePluginDownloadResponse(await downloadResponse.json());
  • parseListPluginsResponse:解析分页列表摘要与清理通告,不包含完整 manifest;
  • parseGetPluginResponse:解析单个 Plugin 详情及当前 Release 的完整 manifest;
  • parsePluginDownloadResponse:解析短期 HTTPS 下载地址及完整性元数据。

当前 Release 摘要可带 icon 元数据。它描述发布时从 .cindy 包中安全提取并独立存储的图标,而不是包内相对路径:

interface PluginIconMetadata {
  mimeType: string;
  sha256: string;
  sizeBytes: number;
  url: string;
  expiresAt: string;
}

iconnull 表示 manifest 未声明图标,或服务端暂未提供图标对象。旧 v2 响应缺少该字段时解析器也规范化为 null,客户端应继续使用兜底图标;提供该字段时,URL 必须是短期 HTTPS 地址,MIME 必须为 image/*,并经过 SHA-256、大小和过期时间校验。

三个解析器校验失败都会抛出 PluginProtocolError,错误消息包含出错字段路径,调用方应把它视为服务端响应不兼容或损坏,不应继续安装或切换 Release:

try {
  const result = parseGetPluginResponse(await response.json());
  // 使用 result.plugin
} catch (error) {
  if (error instanceof PluginProtocolError) {
    // 停止本轮远程对账,保留现有本地安装。
  }
  throw error;
}

解析器返回的对象只保留协议已知字段。列表、详情和下载响应中的 SHA-256 必须是 64 位小写十六进制,字节数必须是正整数,时间必须是带毫秒的 UTC ISO 8601 字符串;下载地址只接受 HTTPS。下载响应不含 schemaVersion,因为它只会在列表或详情 envelope 已成功解析后请求。

清理通告(removals)

列表响应可携带可选的顶层 removals 数组,通告「曾上架、现已下架并要求处置本地副本」的 Plugin。它与 plugins 互补:plugins 只含在架条目,被清理的 Plugin 不会回到列表里;detail 与 download 对被清理的 Plugin 维持 404。

{
  "schemaVersion": 2,
  "plugins": [],
  "nextCursor": null,
  "removals": [
    {
      "pluginId": "cxxxxxxxxxxxxxxxxxxxxxxxx",
      "ghostId": "acme-report",
      "scope": "organization",
      "organizationId": "org_123",
      "action": "purge",
      "removedAt": "2026-08-03T08:00:00.000Z",
    },
  ],
}
  • 这是 v2 的可选追加字段,不提升 PLUGIN_API_SCHEMA_VERSION。老服务端不下发、老客户端按未知字段忽略;解析器在字段缺失或为 null 时规范化为空数组。
  • 服务端只对已验签的组织身份下发其所属组织的通告,与请求的 scope 查询参数无关;当前 scope 恒为 organizationaction 恒为 purge(删除本地已安装副本及插件本地数据)。
  • 分页时每一页都携带完整且相同的 removals,不受搜索关键字与游标影响;客户端聚合分页时按 pluginId 去重。
  • 服务端保证单个响应内 pluginsremovals 不含相同 pluginId,但跨分页请求期间状态可能翻转:客户端应在整轮分页完成后再应用通告;同一轮内某 pluginId 既出现在任一页 plugins 又出现在任一页 removals 时,以在架为准、不执行清理。
  • removedAt 是最近一次下架时间,重新上架再下架会刷新;消费方不得据此假设单调或首次下架时间,去重与匹配一律以 pluginId 为准。
  • action 是取值级前向兼容位:其结构形状固定为 1–64 字符的字符串,形状合法但取值未知的通告会被解析器跳过,不影响其余内容,服务端未来新增动作(动作名必须落在该形状内)不要求客户端同步升级;结构不合法(pluginIdghostId、scope 一致性、action 形状、时间格式)仍抛出 PluginProtocolError
  • 通告不是无条件删除指令:客户端执行前必须与本地安装记录双重校验(pluginId 一致、来源为服务端市场、本地记录的 scope 为 organization),校验不过时最多把该 Plugin 标记为不可更新,不得删除本地内容。

字段语义

字段 语义
Plugin.id plugin-server 生成的永久资源 ID;用于详情、下载和本地 managed marker,不等于包内名称。
ghostId ghost.json.id;在同一 owner 内唯一,不同 Public、Organization、Personal owner 间允许相同。
scope public 对任意已登录 Cindy 身份可用;organization 只对对应组织可用;personal 只对发布者本人可用。
organizationId Organization 必须是非空组织 ID;Public 和 Personal 恒为 null
defaultInstall 对当前请求身份计算后的有效默认安装值;表示未安装时自动安装,不表示强制安装或强制启用。
minCindyVersion Release 的最低 Cindy 版本;必须是合法 SemVer。通常可选且缺失表示兼容所有版本;ios-simulator 等 Host-only slot 可要求必须声明。
X-Cindy-Version 客户端请求列表、详情和下载时携带的 Cindy SemVer;共享常量为 CINDY_CLIENT_VERSION_HEADER,HTTP 头名称大小写不敏感。
currentRelease 服务端为当前客户端选择的 Release;优先服务端 current,不兼容时回退到最新且仍有效的历史兼容 Release。列表只含摘要,详情额外包含 manifest。
currentRelease.icon 所选兼容 Release 的可直接展示图标元数据;为 null 时使用客户端兜底图标,URL 为短期授权地址。
nextCursor 下一页不透明游标或 null;客户端不得解析其内部结构,只能原样回传。当前服务端使用 sortOrder + Plugin.id 的复合位置,并兼容接收滚动期旧 Plugin ID 游标。

parseGetPluginResponse 还会校验 ghostId === manifest.id、Release version === manifest.version、顶层 name/description/author 与当前 manifest 一致,以及声明 oidc-token 的 manifest 只能属于 organization scope。调用方不能用 ghostId 合并不同来源的记录,应以 Plugin.id 标识服务端管理的安装实例。

Cindy 版本兼容

  • supportsCindyVersion 是 Desktop、Plugin Server 和 Forge 的统一比较语义:minCindyVersion 缺失时恒兼容;声明后按 SemVer 比较,当前 Cindy 版本必须大于或等于最低版本。
  • 0.0.00.0.0-* 表示没有正式版本号的开发构建,按当前源码兼容处理。其他非法版本不能参与比较。
  • 老客户端缺少 X-Cindy-Version,或请求头不是合法 SemVer 时,服务端把客户端版本视为未知:仍可交付没有 minCindyVersion 的旧 Release,不得交付声明了最低版本的 Release。
  • 服务端先选择兼容的 current Release;不兼容时,只能从已批准、曾发布并上架且未撤销的历史 Release 中按内部创建顺序回退到最新兼容项。没有兼容 Release 时,列表不下发该 Plugin,详情和下载返回 404。
  • 列表、详情和下载必须使用同一客户端版本选择同一个 Release。Desktop 下载真实 .cindy 包后还要用同一函数复核包内 manifest,不能安装不兼容的包。

版本

  • Ghost manifest 当前只接受 GHOST_MANIFEST_SCHEMA_VERSION=2
  • Plugin HTTP list/detail envelope 当前只接受 PLUGIN_API_SCHEMA_VERSION=2;v2 将 global 替换为 public 并新增 personal
  • 两个版本号独立演进,不能相互替代。

v2 的 nextCursor 语义见上文字段表;解析器接受这种非空、有界字符串,是对既有 v2 线上响应的兼容修复,不是新的 envelope 形状,也不提升 PLUGIN_API_SCHEMA_VERSION

校验器对未知字段保持宽容,对已知字段和值严格校验。新增可选字段不要求服务端和 Desktop 同时发布;破坏性格式变化必须提升对应 schema version。

未知字段只用于前向兼容,不会出现在校验后的返回对象中。消费方不得依赖当前版本未声明的字段。

兼容行为

plugin-server 上传 Release 时使用本包校验 ghost.json,不支持的 manifest 不得发布。客户端使用本包解析列表、详情和短期下载凭证;下载响应本身不重复 envelope 版本,客户端只会在成功解析本轮列表/详情后请求它。下载只进入 staging,Desktop 必须以真实包内 manifest 再次判断兼容性。

客户端不支持 manifest 版本或真实包的 minCindyVersion 时:

  • 首次安装失败并提示当前 Cindy 版本不兼容;
  • 更新失败时丢弃 staging,继续保留本地旧 Release;
  • 不执行 final switch,也不更新本地 managed marker。

HTTP envelope 版本不支持时,客户端停止本轮远程对账并保留本地状态。本期不提供多 current Release、capability 上报或其他协商机制。

Organization 成员上传契约

成员上传是独立于市场 list/detail envelope 的异步发布契约,通过 @cindy/plugin-protocol/member-upload 或包根入口消费。它不改变 PLUGIN_API_SCHEMA_VERSION,也不复用现有 CI raw POST 的请求体。

import {
  PluginProtocolError,
  parsePluginMemberUploadStatusResponse,
} from '@cindy/plugin-protocol/member-upload';

成员上传子路径与 delivery 子路径导出同一个 PluginProtocolError 类,调用方可以稳定使用 instanceof PluginProtocolError 识别解析失败。

操作 HTTP 契约 DTO
prepare POST /api/publisher/uploads PreparePluginMemberUploadRequestPreparePluginMemberUploadResponse
上传文件 PUT <putUrl> 原始 .cindy body;使用 prepare 响应的 headers
commit POST /api/publisher/uploads/:uploadId/commit CommitPluginMemberUploadRequestCommitPluginMemberUploadResponse
status GET /api/publisher/uploads/:uploadId PluginMemberUploadStatusResponse
my-publishes GET /api/publisher/releases/mine ListMyPluginMemberReleasesResponse

Forge 包限制

成员通道统一使用以下权威限制:

常量 含义
PLUGIN_MEMBER_UPLOAD_MAX_ARCHIVE_BYTES 128 MiB 单个 .cindy 压缩包最大字节数
PLUGIN_MEMBER_UPLOAD_MAX_UNCOMPRESSED_BYTES 256 MiB ZIP 条目解压后的累计最大字节数
PLUGIN_MEMBER_UPLOAD_MAX_ZIP_ENTRIES 2048 ZIP 条目数上限

这些常量只约束新的成员上传通道。plugin-server 接入该通道前,既有发布路径继续使用自己的现行限制; 消费方不得因为协议常量已发布,就假定尚未接线的服务端已经接受 128 MiB 包。

Prepare

interface PreparePluginMemberUploadRequest {
  sizeBytes: number;
  sha256: string;
}

interface PreparePluginMemberUploadResponse {
  uploadId: string;
  putUrl: string;
  headers: Record<string, string>;
  expiresAt: string;
  status: 'awaiting_upload';
}
  • sizeBytes 必须是 1..PLUGIN_MEMBER_UPLOAD_MAX_ARCHIVE_BYTES 的安全整数;sha256 是 64 位小写十六进制。
  • putUrl 必须是 HTTPS;调用方按响应提供的 headers 上传,不把 Connection JWT 注入对象存储地址。
  • organization、membership、passport、ghostId、version、对象 key 和幂等身份都不在 body 中。身份只来自服务端已验证的鉴权上下文;幂等键使用 Idempotency-Key 请求头。
  • 相同身份范围和 Idempotency-Key 的 prepare 重放返回同一 upload session,不创建第二份发布。

Commit

type CommitPluginMemberUploadRequest = Record<string, never>;

interface CommitPluginMemberUploadResponse {
  uploadId: string;
  status: PluginMemberUploadStatus;
}

commit body 必须为空,不能覆盖 actor、organization、ghostId、version、hash、大小或对象位置。 commit 只启动或复用持久化异步任务;相同身份范围和 Idempotency-Key 的重放必须观察同一任务/result, 不能重复创建 Release 或审计记录。

Status 与 my-publishes

interface PluginMemberUploadStatusResponse {
  uploadId: string;
  status: 'awaiting_upload' | 'validating' | 'publishing' | 'succeeded' | 'failed' | 'expired';
  pluginId: string | null;
  releaseId: string | null;
  ghostId: string | null;
  version: string | null;
  reviewStatus: 'pending' | 'approved' | 'rejected' | null;
  failure: { code: PluginMemberUploadFailureCode; message: string } | null;
}

interface PluginMemberReleaseSummary extends PluginMemberUploadStatusResponse {
  createdAt: string;
  updatedAt: string;
}

interface ListMyPluginMemberReleasesResponse {
  releases: PluginMemberReleaseSummary[];
  nextCursor: string | null;
}
  • 上传任务状态与 Release 审核状态相互独立。succeeded 必须同时带非空 pluginId/releaseId/ghostId/version/reviewStatus;其他任务状态的 reviewStatus 必须为 null
  • failed 必须带 failure,其他任务状态不得带;message 是处理后的用户可见原因,不得包含内部对象 key、审计 detail、凭证或签名 URL。
  • 稳定异步失败码为:UPLOAD_OBJECT_MISSINGUPLOAD_SIZE_MISMATCHUPLOAD_SHA256_MISMATCHPLUGIN_PACKAGE_INVALIDMEMBERSHIP_INACTIVEPUBLISH_NOT_AUTHORIZEDPLUGIN_GHOST_ID_CONFLICTPUBLISH_STORAGE_UNAVAILABLEPUBLISH_INTERNAL_ERROR
  • 非空 ghostId 必须满足与 ghost.json.id 相同的 isValidGhostId 规则。
  • 非空 pluginId 必须满足与市场 delivery 响应相同的 isValidPluginResourceId 规则。
  • createdAt/updatedAt/expiresAt 均为带毫秒的 UTC ISO 8601 时间。
  • nextCursor 是 1–4096 字符的不透明字符串或 null;调用方只能原样回传,不得从中推导成员、Release 或时间信息。
  • my-publishes 只包含当前已验证成员有权查看的发布记录;身份和筛选范围不接受 body/query 自报覆盖。

Rollout 与降级边界

  • 此协议包先提供唯一 DTO、限制和解析规则;plugin-server、Cindy Host 和发布者插件分别 bump 后才能开启成员上传。
  • 在三方实现和部署完成前,成员发布入口必须保持关闭;不能让任一消费方靠手抄字段或数字提前接线。
  • 新成员上传解析器对已知字段和值 fail-closed。状态响应不合法或 PUT 结果不确定时,调用方不得把任务当成功,也不得自动重放 prepare/commit;应先重新查询服务端 status。
  • 该契约不改变既有 GitHub OIDC CI 发布路径,也不改变市场 list/detail/download 的 v2 兼容策略。

消费顺序

本期由 plugin-server 和 Desktop 共同消费该包。协议合并后,两个消费方仓库分别 bump submodule 指针并在各自 PR 中完成适配;不能让任一方长期停留在只认识旧 manifest 的版本。