书接上文 『OSPP2023』我与 OSPP 的故事 —— 从听闻到中选 ,本文注重于描写项目开发的经历


项目基本信息

  • 项目名称:为 Envoy Go 扩展建设插件市场

  • 项目导师:纪卓志

  • 项目描述:

    Envoy 是当前最流行的网络代理之一,Go 扩展是 MOSN 社区为 Envoy 增加的 Go 生态基础,也是 MOSN 社区 MoE 框架的基础。

    受益于Golang生态系统,研发可以轻松在 Envoy 实现插件用于更多的长尾场景,其中很多场景都是通用的。

    本项目是为Envoy Go 扩展构建插件市场。在插件市场中,人们可以在插件市场中分享插件,选用已经存在的插件。通过插件市场,可以让 Envoy、MoE 生态变得更加开放、共享、丰富。

  • 项目链接:

    https://summer-ospp.ac.cn/org/prodetail/23f080259?lang=zh&list=pro

项目迭代经历

首先我想说这个项目的架构设计经历了多次变动,发现最后做出来的和最开始想的根本不是一个东西(笑)

一开始 OSPP 上简短的描述并不能让我了解太多,于是我开始翻 MOSN 的文档,并且和导师在 Issue 下交流更为详细的需求

需要实现的是一个插件市场,也就是类似于 VSCode Marketplace 或者 GitHub Marketplace 的效果,含插件提交、审核、版本管理和二进制构建分发等

image-20231017下午32737287

我的构想是分为三个部分,GitHub、后端本体还有镜像仓库

开发者在自己的仓库里开发,如果要上架的话需要移交仓库权限到官方组织里,发布新版本就正常 Release,可以通过 GitHub Actions 通知后端或者由后端来轮询同步

后端同步仓库情况,维护插件列表与各自的历史版本,同时对每个版本都编译并推送镜像

v1.全局设计

而对于后端内部,我当初打算搓一套微服务,还画了微服务拆分与架构图(

v1.微服务拆分

v1.后端架构

然而项目的难点实际上不在于要用什么什么复杂的技术,而在于整体的面向用户的设计,能有一套实际落地可执行的方案

image-20231017下午104608175

感谢导师及时为我指明方向 ❤️

我觉得导师说的的确很有道理,需要交付 MVP 最小价值产品,它指的是产品在满足基本功能需求的前提下,具有的最小的功能集合

于是,后端肯定是一个简单单体了,并且仓库变成了各自独立开发,目前主要的工作放在细化整个链路上的各个环节

OK,我们的目标是搭建一个插件市场,插件本体作为 Docker Image 的形式分发,为了达到这个目标,我们需要解决几个子问题

  • 提交流程该如何设计
    • 上架一个插件,用户需要提交哪些内容?
  • 审核流程该如何设计
    • 哪些行为需要审核?
    • 如何审核,自建审核系统,还是依托其他服务?
  • 如何进行版本管理
    • 如何上架新版本
    • 如何下架一个版本
  • 元信息(名称描述分类等)如何存储
    • 是全部都存在插件本体中,然后平台后端全量缓存
    • 还是一部分打包在插件中,另一部分单独在后端保存
  • 插件本体(Docker镜像)该如何构建,存储与分发
    • 是我们负责构建存储分发一条龙
    • 还是交给用户存储在第三方(如 Docker Hub),我们只保存地址?

自己摸索无异于闭门造车,我认为可以参考一下其他平台的做法

因此,我研究了 8 个类似的或者可以提高参考价值的平台,观察他们是如何解决这些问题的,总结为下表:

平台/市场 提交流程 审核机制 版本管理 验证官方发布 元信息保存 官方文档
Visual Studio Code Marketplace 1. 将插件打包为 .vsix 文件
2. 在 Azure DevOps 使用Microsoft 帐户创建账号并获取 Token
3. 使用同一Microsoft 帐户在 Visual Studio Marketplace 创建发布者
4. 使用 vsce loginvsce publish 命令发布插件
在 Visual Studio Marketplace 上由 Marketplace 团队进行审核 1. 更新 package.json 的版本号
2. 使用 vsce publish 发布新版本
3. 无法直接删除特定版本,需要联系官方支持团队进行处理
通过验证是否持有该公司/组织的域名 保存在项目的 package.json
包含唯一标识、名称、描述、版本号、发布者名称、兼容的 VSC 版本、分类、关键词等
Publishing Extensions | Visual Studio Code Extension API
JetBrains Plugin Repository 1. 在官网上创建账号
2. 在创建插件页面上传插件 JAR/ZIP 文件并发布
由 JetBrains 团队进行审核 1. 上传新的插件文件
2. 在“Versions”选项卡中删除旧版本
暂无明确的官方验证机制 大部分保存在项目的 plugin.xml 中,但分类是在发布的页面手动指定 Uploading a new plugin | JetBrains Marketplace Documentation
GitHub Actions 1. 在 GitHub 仓库中完成开发,并编写 action.yml
2. 创建一个 release ,勾上发布到市场,同时填写分类等信息
无审核机制,发布后即可使用 1. 创建新的 release, 并勾上发布到市场
2. 如要删除发布,取消勾选并保存即可
仓库属于哪个组织,就是由哪个组织发布的 大部分保存在项目的 action.yml 中,但分类是在发布的页面手动指定 在 GitHub Marketplace 中发布操作 - GitHub 文档
Chrome Web Store 1. 在 Chrome 开发者仪表盘创建新项目
2. 上传 .zip 文件包含扩展的所有代码
3. 填写项目详情,如名称、描述、图标、预览图等
4. 提交审核并支付开发者注册费用
由 Google 团队进行人工审核,内容涵盖性能、安全、隐私等方面 1. 在开发者仪表盘提交新的 .zip 文件
2. 旧版本不会被自动删除,用户可以在商店中查看所有版本
通过 Google 账户验证 一部分保存在项目的 manifest.json 中,如名称、版本号、描述等
其余信息如图标、预览图、详细描述在开发者仪表盘填写
创建和发布自定义 Chrome 应用和扩展程序 - Chrome Enterprise and Education帮助
Apple App Store 1. 注册成为 Apple 开发者并支付年费
2. 使用 Xcode 开发应用并配置相关信息
3. 在 App Store Connect 上创建应用并上传
4. 提交审核请求
由 Apple 团队进行严格审核,包括功能、安全性、隐私、设计等方面 1. 在 Xcode 中更新版本号和构建号
2. 在 App Store Connect 上上传新版本并提交审核
3. 旧版本自动下架
通过 Apple Developer Program 验证身份 最基础的部分保存在项目的 Info.plist 中,如版本号,构建号,唯一标识,设备上显示的名称
另一部分是在 App Store Connect 上,包括在App Store上的应用名称、描述、版本号、类别、预览截图等
将 iOS App 提交至 App Store - Apple Developer
WordPress Plugin Repository 1. 在官网上注册账号
2. 在 SVN 仓库中添加插件代码
3. 使用 Readme Validator 验证 readme.txt
4. 在官网上提交插件并等待审核
由 WordPress 团队进行审核,主要关注插件的功能和安全性 1. 在 SVN 仓库中更新插件和 readme.txt 的版本号
2. 在官网上标记新版本的发布
3. 旧版本仍然可用
没有明确的官方验证机制 保存在项目的 readme.txt 中,包括名称、描述、版本号、作者、标签等 zh-cn:开发一个插件 « WordPress Codex
Docker Extension
1. 在Docker Hub上注册账号
2. 构建好你的扩展镜像,并提交到 Docker Hub
3. 选择一种发布方式并等待审核
可以选择自行发布或者请求官方审核 1. 推送新版本的扩展 Docker Image ,并带有递增的版本标记
2. 像管理你的镜像版本一样管理你的扩展版本
加入 Docker Verified Publisher Program 保存在扩展镜像中的 metadata.json 中,当然官方肯定也会缓存一部分 Publish your extension to the marketplace
ChatGPT Plugins 1. 搭建好你的 API 服务
2. 以官方的格式创建 JSON/YAML 文件描述你的插件,并保存在域名下
目前是在官网通过机器人递交表单,然后人工审核 1. 更新你部署即可
2. 不需要维护多版本,访问到的就是你部署的最新版本
暂无明确的官方验证机制 大部分保存在你部署的 API 的域名下的 YAML/JSON 文件中,官方服务器仅保存名称,描述及域名等基本信息 Getting Started - OpenAI API

依此,我进行以下设计(下面节选自 Proposal)

  • 在提交流程这一步来看,我感觉 GitHub Actions 的流程和我们的项目是最贴切的,毕竟我们希望尽可能地利用 GitHub 的基础设施,我认为我们也可以将仓库的一个 Release 关联到插件的一个版本

  • 审核机制来看,大部分都是平台自建审核功能,而我也注意到了Docker Extension 的 自行发布 的做法,我认为可以借鉴他的做法:他是一个 Issue Form 对应一个申请,然后,用 tag 标记状态,并由 GitHub Actions 自动检查是否符合条件,这种做法让我想起来社团里有学长加友链也是这么搞的, Issue Form 真的可玩性挺高的

    而对于我们来说,可以根据审核的事件类型自定义要不要加人工审核,比如说上架,机器人检查通过之后(这个 repo 的确按照我们规定的格式编写好了插件,可以编译成 Docker Image),可以自动 @ 管理员来人工审核并通过(更改 tag 并关闭 issue),如果有问题则可以在这个 issue 下面继续交流

  • 版本管理来看,可以使用与 GitHub Actions 一样的关联 Release 的做法,然后上架或者下架新版本都需要提交审核申请,添加/解除与一个 Release 的关联

  • 验证官方发布来看,也可以和 GitHub Actions 一样,仓库在谁手里就是谁发布的,当然还可以以提交审核的方式认证一些别的 tag

  • 关于元信息存储,我的看法是与 Visual Studio Code 一样在本体中全量存储(比如在根目录的 metadata.json ) ,然后后端数据库缓存一份,并且始终缓存最新版的信息,这种做法对后端应该最方便,但是对用户来说可能有点麻烦,毕竟你要改描述或者分类这种信息也需要再发布一个新版本

    也就是说,只要插件的仓库中有一个能编译出来镜像的 Dockerfile ,以及一个符合规范的 metadata.yaml ,就够了

  • 插件本体存储来看,除去 ChatGPT Plugins 提交的是 API 之外,其他的都是提交并分发一个能离线运行的实体,有些实体是不需要编译的,直接提交源码即可,有些是提交了编译后的产物(如 JAR 包),而在这些案例中大多数都是直接提交编译后的二进制,但是 Docker Extension 有所不同,他是让用户自行将 Docker Image 提交到 Docker Hub , 然后提交扩展的时候就上交一个链接就好了,如果我们也这样做的话就是把存储成本转嫁给用户,但是从稳定性来看感觉不妥,当然你也可以说 Docker Hub 是他们自家的存储设施,我的结论就是由我们自己负责编译和存储

另外,作为一个市场还可以有评分和评论的功能,但是我感觉没什么必要,评分的话看仓库的 star 应该就可以了,如果对插件有什么看法的话也可以直接去提一个 issue,当然如果要做的话也可以用 giscus 这种解决方法,直接依托 GitHub 的基础设施

而核心的上架流程是这样的

  1. 在自己的 GitHub 仓库中开发好插件,包含 Dockerfilemetadata.yaml

  2. 使用 Issue Form 提交申请,包括自己的仓库地址,同意服务条款

  3. 使用 GitHub Actions 检查是否接收了服务条款

  4. 使用 GitHub Actions 找到该项目的 Latest Release ,检查是否合规

    1. 元数据是否符合规范、完整

      1. 询问后端是否重名
      2. 是否填写了分类、分类是否在预定义的种类中
      3. 如果定义了 icon、color ,定义是否合规
    2. 是否能够编译

      使用 docker build 试编译

  5. 自动检测通过,打上 tag , 等待人工审核

    若未通过,告知原因,并告知需要发布新版本并使用 /validate 重新检测

  6. 人工审核并更新 tag

  7. GitHub Actions 识别到通过的 tag, 向后端上报要关联到的 release 版本

  8. 上架完成后自动关闭 issue

前文所说的 Docker Extension 的 自行发布 我感觉设计的真的很好,最大的特点就是使用 Issue Form 进行申请,然后 GitHub Actions 会自动跑一遍检查,并在 Issue 页面使用对话的形式与用户交互,如果有错误也会指出,并使用 tag 追踪状态

image-20231017上午113215497

image-20231017上午113155311

image-20231017上午113133442

这一设计最大的好处就是公开透明,所有与用户的交互都是公开的,并且就在 GitHub

很巧的是我们社团的 Marlene 同学的 OSPP 项目也是做插件市场,我和他在私下讨论了这种做法,他也表示很认同,并打算借鉴这种方案

于是总体设计就变成了这个样子

v4.全局设计

差别不算大,但是变成了每次发版都要新增一个 Issue 并进行两段式审核,一次机审是看符不符合规范,第二次是人工审核,也就是要求中的「经过社区 review」

之后就是 OSPP 中选了,中选后被社区拉着开了次会

image-20231017下午42651938

有点后悔没录像,感觉开的挺好的

会上我讲解了现在的方案,有一个问题被重点讨论,就是不再使⽤分库的形式(开发者在各⾃的仓库维护项⽬),⽽是⼤库(所有⼈向主仓库提交 PR)

这个改变有几个原因,我记得一方面能增加开发者的认同感(我给官方仓库贡献了一个插件),另一方面这个平台应该是我们与开发者共建,而不是我们去服务开发者(有点怪,但大概是这个意思)

于是,使用大库的话很多逻辑就又要改了,比方说版本管理要自己设计一套方案,我当时设计的是在元数据里记录版本,然后版本变动的时候就通知后端更新数据,把本次 commit hash 和这个插件的这个版本关联起来,再去做 bulid

为什么要做版本管理呢?这个在会上我也讨论过,比如 VSCode 的插件就有版本管理,它每个插件版本都在元数据写上能支持哪些 VSCode 版本,我觉得我们这个 MoE 的基座也会存在类似的问题

项目结构大概就是这个样子

1
2
3
4
5
6
7
8
9
10
11
.
├── LICENSE
├── README.md
├── plugins # 插件⽬录,存储所有插件
│ ├── example
│ ├── example2
│ ├── example3
│ └── example4
└── web
├── frontend # 前端
└── backend # 后端

每个插件目录的必须内容如下

1
2
3
4
5
example
├── changelog.md # 开发者⾃⼰维护的 Markdown
├── config.proto # 配置参数定义
├── metadata.yaml # 插件元数据,包括名称描述分类版本等
└── readme.md # 开发者⾃⼰维护的 Markdown

这样一来,后端就需要 clone 和 pull 这个仓库,再深挖 git 历史,分析各个插件的历史版本,有点费力

为什么要这么搞呢?有没有更简单的方法,可能吧,但是我没找到

  • Docker Extension 交的本来就是 Docker Image 的地址,Docker Hub 能给他做版本管理
  • 而 Marlene 那是 PyPI ,版本管理那边已经做好了

当然大库也不是全然没好处,我感觉这样就可以省一些逻辑,比方说貌似不需要 Issue Form 来处理各个发版事件了,也不需要维护一堆 tag,每个人直接对着仓库 PR 就行,只需要一个 Actions 里有一个简单的检查,不需要糊交互

想好了之后我开始想前后端了,MVP 的话,感觉两个接口就行,一个列表一个详情即可

而后端还需要数据库吗?啊,貌似数据库也不需要了,可以每次更新的时候直接生成 JSON 缓存,然后直接返回给前端,而查询的话至少近期可以在前端进行,这样貌似根据稳定

而 build 的话,我打算暂时先放一放吧,做最简单的打包某版本的源代码,至于编译上传 S3,或者使用 Docker Image 可以后面再说

同时,我开始糊前端,先画设计图

429cdbfb0271c2ced239bae81e6748fc

主⻚包含列出,搜索、分类功能,详情页包含 Overview、Config、Version和 Changlog 四个 Tab

后端感觉没什么问题我就开始搓前端了,但前端我其实不是很会,但是还是在前端大佬 daidr 的帮助下完成了

首页

详情页的 Overview ,是原 readme.md 的渲染

但这个渲染并不是简单的渲染,我是调用了 GitHub 的 Render a Markdown document 接口,结合了仓库的上下文

(⽐如 #1 会被正确指向对应的 Issue 或者 PR)

Config 是原 config.proto 的渲染

Changelog 是原 changelog.md 的渲染

Versions 包含插件的所有历史版本,这个在 GitHub 直接发一个 Release 是社区里其他导师建议的

而调试到了最后,居然感觉连后端都可以不要了(

首先由于域名备案等考虑需要,⽬前不需要有评论等交互功能,OAuth 登陆⽬前没有意义,有也可以使用静态博客的评论系统,而流量统计使用第三方服务也可以做到,至于编译推送,也可以在 GitHub Actions 中完成(我之前感觉从公网推送到内网不是很合适,但现在感觉应该也就是推到某些第三方服务上了)

image-20231017下午91551486

好好好

于是我开始把原来后端的逻辑塞到 GitHub 工作流中再重构,再进行若干修改

我想到了可以直接以 RAW 的形式直接读取仓库里的 JSON 文件,就像我的一个玩具项目一样

7a6ab5853fd4d20854d3bdc491a72fd4

其实没后端反而更方便了,因为我大概只需要执行这段伪代码

1
2
3
4
5
for 插件 := range 所有插件 {
if 当前插件的版本没有打过tag {
发布并更新缓存
}
}

此外还有些小改动,就是把生成的缓存移到其他分支中,因为 main 分支大概会有保护规则,不允许直接提交

于是我将缓存移到 cache 分支,并用将前端用 GitHub Page 发布,这下是真不要钱了(

再不断的调试后,到这一步,整个项目其实已经应该能跑了,而令我没想到的是,回看最开始的产出要求,我其实一个都没做到(笑)

  1. 提供一个Envoy Go 插件的内容平台,在这里可以发布经过社区review的优秀插件,需要拥有服务端与前端页面
  2. 不自建账号体系,通过GitHub OAuth2.0完成用户认证与授权
  3. 进阶 - 对接GitHub OpenAPI,支持动态获取插件所在仓库信息,包括README,分支版本以及star数

乐,只能说计划没法赶上变化

除此之外,还令我没想到的是导师会提那么多 LICENSE 相关的问题(

08b0a845b2c2c0a642d8116684a9e502

397e42e1900775a5c99f0cfc6f6428ed 86a757d0c923feb50298e40519ea5223

我大概了解各自开源协议的含义,但还从没认真对待过

至于未来这个项目会怎么发展,导师说还可以用 Docusaurus 做生成的静态前端,我想也是,这样就不用生成 cache 了,直接更新前端,能少一次请求