Skip to content

插件清单格式 ​

每个插件包的根目录必须有 manifest.json。没有清单 = 拒绝加载,不做兜底猜测——插件包是要被第三方分发的东西,字段含糊就意味着行为含糊。

一个 1.x 最小清单 ​

仓库里的示例插件(examples/plugins/hello-panel/manifest.json)长这样:

json
{
  "id": "com.example.hello",
  "name": "Hello 面板",
  "version": "1.0.0",
  "apiVersion": "1.0",
  "author": "Starcloud 示例",
  "description": "示例插件:注册面板 + 启动参数追加(仅规范允许的能力)",
  "permissions": [
    "launcher:game_arguments",
    "launcher:notification",
    "launcher:ui_panel"
  ],
  "optionalPermissions": [],
  "compatibility": { "minLauncherVersion": "0.2.4" }
}

必填的只有五项:id、name、version、apiVersion、author。permissions 缺省等于零权限,插件照样能加载——只是什么也干不了。

没有 main 字段

1.x/2.0 的清单不声明入口文件。入口是推断出来的:包里有 main.js 就走 JS 运行时,有 main.py 就走 Python,都没有但声明了 contributes,就当作纯声明式插件(无可执行代码,钩子全是空操作);都没有也没 contributes 直接报「插件缺少入口脚本」。

id 的命名规则 ​

id 是反域名格式,不是域名格式:

csharp
public static bool IsValidPluginId(string id) =>
    !string.IsNullOrWhiteSpace(id) && id.Length <= 64 &&
    Regex.IsMatch(id, @"^[a-zA-Z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$");

也就是必须至少有一个点,且不能以点开头、不能是 com.example 这样的纯域名形式,≤64 字符。

为什么不是域名?因为 com.example.hello 里的 example 不是域名所有者,是命名空间。用域名形式会让人误以为 id 全局唯一且可反查注册商——实际上唯一性由宿主保证,跟域名注册无关。

id 一旦发布就不能改:它决定插件的私有目录名(plugin-data/<id>、plugin-config/<id>.json)、导航项 tag(plugin:<id>:<navId>)、存储键前缀。改 id 等于换了一个插件。

路径安全 ​

清单里所有指向包内文件的字段(contributes.*.view、contributes.settings.schema、itemView)都必须过这一关:

csharp
public static bool IsSafeRelativePath(string path)
{
    var norm = path.Replace('\\', '/');
    if (Path.IsPathRooted(norm) || norm.Contains(':')) return false;
    return !norm.Split('/').Contains("..");
}

三样东西被拒:绝对路径(/etc/passwd)、盘符(C:\...,靠含 : 判定)、.. 穿越。校验失败是拒绝整份清单,不是跳过这个字段——留一半生效的清单比全盘拒绝更难排查。

2.0 的清单 ​

apiVersion: "2.0" 走另一套校验分支,字段也换了一批:

json
{
  "id": "com.example.myplugin",
  "name": "My Plugin",
  "version": "2.1.0",
  "apiVersion": "2.0",
  "author": "Example",
  "engines": { "starcloud": ">=0.4.0 <1.0.0" },
  "runtime": "js",
  "permissions": ["instances:read", "network:host=api.example.com", "ui:panel"],
  "dependencies": { "com.example.lib": "^1.2.0" },
  "conflicts": { "com.example.rival": "*" },
  "contributes": {
    "panels": [ { "id": "main", "title": "我的面板", "view": "panels/main.json" } ],
    "navItems": [ { "id": "servers", "title": "服务器浏览", "icon": "server", "view": "views/servers.json" } ]
  }
}

几个容易踩的点:

  • 2.0 的 version 必须是严格语义化版本(主.次.修订),1.x 不强制。校验用的是正则 ^\d+\.\d+\.\d+([-+][0-9A-Za-z.\-]+)?$。
  • engines.starcloud 是区间,取代 1.x 的 compatibility.minLauncherVersion。后者仍被接受,当等价下限用。
  • runtime 只能是 js / wasm / py,但 wasm 运行时目前没有实现——写了也只是清单能过,实际加载不到。
  • 权限语法在 2.0 换了(launcher:network → network:host=...)。在 2.0 清单里写 1.x 写法会被明确拒绝并告诉你语法已变更。

版本区间 ​

engines.starcloud、dependencies、conflicts 用的是 npm 风格区间,解析器在 PluginVersionRange:

写法含义
1.2.3精确等于
^1.2.3>=1.2.3 <2.0.0
~1.2.3>=1.2.3 <1.3.0
>=0.4.0 <1.0.0空格分隔的比较符组合(逗号也行)
* / x / latest任意版本

^ 和 ~ 的上界按 npm 语义算:^0.2.3 是 <0.3.0,^0.0.3 是 <0.0.4——0 开头的版本号锁得比 1.x 更紧。

比较只按数字段走,预发布和构建元数据尾缀(-beta.1、+build)被剥掉不比。1.0.0-beta 和 1.0.0 在区间判断里是同一个版本,想要区分预发布目前做不到。

依赖与冲突 ​

dependencies 声明「我需要谁」,conflicts 声明「我和谁不能共存」,两者用同一套区间语法。

冲突的判定规则有个不对称:声明 conflicts 的那一方被阻断,不会自动停用对方;双向都声明则双方都停。这样规则是确定的——不会出现「谁先加载谁赢」这种依赖加载顺序的结果。

依赖缺失、未启用、版本不满足、自身不可用——每种阻断都有独立文案,直接显示在插件页上,不用猜。

相关链接:权限模型 · 声明式界面 · 插件与内核

基于 MIT 许可证(有修改)开源