Skip to content

插件权限 ​

插件通过清单声明所需能力,由用户在启用时决定是否授权。本文说明权限声明、查询、失败处理和生命周期;项目接入见 开发入门。

Gradle 声明 ​

kotlin
import com.xuncorp.spw.workshop.gradle.PluginPermission

spmod {
    PluginClass = "com.example.MyPlugin"
    PluginId = "com.example.my-plugin"
    PluginVersion = "1.0.0"
    PluginPermissions = listOf(PluginPermission.KEY_BINDINGS, PluginPermission.LIBRARY_READ)
}
groovy
import com.xuncorp.spw.workshop.gradle.PluginPermission

spmod { config ->
    config.PluginClass = 'com.example.MyPlugin'
    config.PluginId = 'com.example.my-plugin'
    config.PluginVersion = '1.0.0'
    config.PluginPermissions = [PluginPermission.KEY_BINDINGS, PluginPermission.LIBRARY_READ]
}

PluginPermissions 的类型为 List<PluginPermission>,默认空列表,未设置或设为 null 表示不申请权限。列表只接受插件提供的枚举,重复项会去重;Kotlin DSL 传入字符串会产生脚本编译错误,Groovy 的动态列表包含字符串或 null 元素时会报告明确的配置错误。

Gradle 枚举Manifest 标识API 常量能力
PluginPermission.KEY_BINDINGSkey-bindingsPluginPermission.KEY_BINDINGS注册应用内快捷键,并可通过 hasGlobal 允许用户自行配置全局快捷键
PluginPermission.LIBRARY_READlibrary-readPluginPermission.LIBRARY_READ查询曲库歌曲元数据、文件路径、收藏状态和内嵌封面,包括当前歌曲的元数据查询
PluginPermission.LIBRARY_WRITElibrary-writePluginPermission.LIBRARY_WRITE写入曲库数据的独立权限;为后续接口预留,当前 API 尚无写入入口
PluginPermission.CLASS_TRANSFORMclass-transformPluginPermission.CLASS_TRANSFORM注册宿主方法与 UI Hook 以改变应用行为;最高危权限,仅应授予完全信任的插件

读写权限互不包含。插件只应申请实际使用的权限;当前曲库查询只需声明 LIBRARY_READ,无需申请 LIBRARY_WRITE。

方法与 UI Hook ​

声明 CLASS_TRANSFORM 并获授权后,插件可通过 WorkshopApi.hookRegistrar 注册方法与 UI Hook,停用、卸载或撤销权限后停止新回调并恢复原行为。这是最高危权限,仅应授予完全信任的插件;权限清单标识沿用 class-transform。

Kotlin DSL、Java 回调与约束见 直接 Hook API,插件无需提供字节码转换器。

查询与失败处理 ​

WorkshopApi.manager.isPermissionGranted(permission) 同步查询实际调用插件的授权,无弹窗或加载副作用,可在任意线程调用。未声明、未授权、无法识别调用者或调用类加载器已经卸载时返回 false。缓存或传递 Manager 引用不会改变查询归属。

运行时代码使用 com.xuncorp.spw.workshop.api.PluginPermission,Gradle 配置使用 com.xuncorp.spw.workshop.gradle.PluginPermission,两者不要混用。下例的 openPanel() 由插件自行实现,注册应直接在插件的 start() 线程中完成。

kotlin
override fun start() {
    if (WorkshopApi.manager.isPermissionGranted(PluginPermission.KEY_BINDINGS)) {
        try {
            WorkshopApi.manager.keyBindingManager.register(
                actionId = "open-panel",
                title = "打开面板",
                defaultShortcut = null,
                handler = Runnable { openPanel() }
            )
        } catch (denied: PluginPermissionDeniedException) {
            // 保留插件的其他能力,停用快捷键功能
        }
    }
}
java
@Override
public void start() {
    if (WorkshopApi.manager().isPermissionGranted(PluginPermission.KEY_BINDINGS)) {
        try {
            WorkshopApi.manager().getKeyBindingManager().register(
                    "open-panel",
                    "打开面板",
                    null,
                    false,
                    this::openPanel
            );
        } catch (PluginPermissionDeniedException denied) {
            // 保留插件的其他能力,停用快捷键功能
        }
    }
}

未声明或未经授权时,register 同步抛出 PluginPermissionDeniedException,异常的 pluginId 与 permission 标明被拒绝的插件及所需权限。该异常继承 SecurityException,Java 可通过 getPluginId()、getPermission() 读取。调用归属、注册时机和参数约束仍适用;注销与关闭注册句柄无需权限。

曲库读取 ​

WorkshopApi.library 的 getTrackById、getAllTracks、getTracks、getCoverById 均要求 LIBRARY_READ。权限不足时返回失败的 CompletionStage,不会启动数据读取。宿主在调用线程识别插件,在读取前与交付结果前重新校验权限和加载身份;缓存或向其他插件传递 Library 引用不会转移授权。

运行时代码导入 com.xuncorp.spw.workshop.api.PluginPermission。

kotlin
if (WorkshopApi.manager
        .isPermissionGranted(PluginPermission.LIBRARY_READ)
) {
    WorkshopApi.library.getTracks(afterId = null, limit = 20)
        .whenComplete { tracks, failure ->
            val cause = (failure as? java.util.concurrent.CompletionException)?.cause ?: failure
            when (cause) {
                null -> tracks.forEach { println(it.title) }
                is PluginPermissionDeniedException -> println("曲库读取权限不可用")
                else -> cause.printStackTrace()
            }
        }
}
java
if (WorkshopApi.manager()
        .isPermissionGranted(PluginPermission.LIBRARY_READ)
) {
    WorkshopApi.library()
        .getTracks(null, 20)
        .whenComplete((tracks, failure) -> {
            Throwable cause = failure instanceof java.util.concurrent.CompletionException
                   ? failure.getCause() : failure;
            if (cause == null) {
               tracks.forEach(track -> System.out.println(track.getTitle()));
            } else if (cause instanceof PluginPermissionDeniedException) {
                System.out.println("曲库读取权限不可用");
            } else {
                cause.printStackTrace();
            }
        });
}

主动查询权限只用于功能降级,不能替代失败处理。Playback.getCurrentMediaItem() 也需要该权限,即使当前没有歌曲:调用时未授权会同步抛出 PluginPermissionDeniedException,查询期间的权限拒绝通过 CompletionException 的 cause 返回。无法识别调用者或已卸载类发起的新调用也会拒绝,异常的 pluginId 为 unknown。

停用插件保留授权,已接受的查询可以继续完成;交付结果前撤销授权或卸载插件会令查询失败。同 ID 插件重新加载后,旧调用与旧类不能复用新加载实例的权限。已经成功交付的快照不会被收回。取消转换出的 future 不保证取消底层读取。

用户决定与生命周期 ​

  • 首次启用时,在宿主现有的启用弹窗中显示权限及用途,新权限默认不勾选。用户可以不授予权限而启用插件,插件应据查询结果保留可用功能;未处理的注册拒绝异常会导致启动失败
  • 确认后保存授权与拒绝决定,再调用插件 start();取消弹窗不改变已保存决定
  • 重启、停用和普通更新保留决定。可停用插件后在启用弹窗中调整勾选
  • 更新新增权限时暂停自动启动,等待用户重新确认;新权限不会自动授予。依赖链中存在待确认权限时也不会自动启动,需先确认相应依赖插件
  • 未知权限在启用弹窗中标记为不支持,无法授予
  • 删除本地插件或取消 Steam 订阅会清除决定,再次安装需要重新授权;卸载类加载器以更新插件时保留决定
  • 插件必须为受保护的能力补充 Gradle 权限声明,并处理用户拒绝授权的情况

API 的查询方法提供返回 false 的 JVM 默认实现,以兼容旧的 Manager 实现类;这不使新消费者能够在缺少该方法的旧 API JAR 上运行。

此机制控制 Workshop API 提供的宿主能力。当前插件仍在宿主 JVM 内运行,权限声明和检查不构成操作系统沙箱。

相关文档:开发入门 · 插件配置 · 直接 Hook API。

最近更新