Skip to content

PluginInstaller

PluginInstaller 把市场里的插件装到用户设备上。流程:下载 ZIP → 解压 → 校验 manifest → 复制到 plugins root → 返回结果。安装完成后由调用方调 ref.invalidate(pluginManagerProvider) 触发 PluginManager.refresh()。

文件:lib/core/plugin/plugin_installer.dart

class PluginInstallResult {
final bool success;
final String? error;
final List<String> installedIds;
}
字段 说明
success 是否成功
error 失败原因(成功时 null)
installedIds 成功安装的插件 id 列表(Friend/Application 1 个,Pack 多个)

构造器:

const PluginInstallResult.ok(List<String> installedIds);
const PluginInstallResult.fail(String error);
class PluginInstaller {
final MarketRepository repository;
PluginInstaller(this.repository);
}
Future<PluginInstallResult> install(
MarketPlugin plugin, {
void Function(double progress)? onProgress,
})

安装一个市场插件。onProgress 在下载阶段收到 0.0–1.0 的进度(解压/复制阶段不调)。

  1. 下载:repository.downloadPackage(dir, id, onProgress)——走磁盘缓存(永久),未命中才流式下载
  2. 解压:ZipDecoder().decodeBytes(packageBytes),逐条目解压到临时目录
    • 拒绝绝对路径(/ 开头)和 ..(防 zip-slip 攻击)
    • 只处理文件条目(跳过目录条目,创建父目录即可)
  3. 校验 manifest:读临时目录的 manifest.json
    • 必须存在且是 JSON 对象
    • id 必须和 MarketPlugin.id 一致
    • type 必须和 MarketPlugin.type 一致
  4. 复制到 plugins root:
    • Friend / Application:copyDirectory(tmpDir, <pluginsRoot>/<safeId>.mood*)
    • Pack:installPack(tmpDir)——展开各子插件目录 + 整合包目录
  5. 返回 PluginInstallResult.ok(installedIds)

任何步骤失败返回 PluginInstallResult.fail(error),不抛异常。临时目录在 finally 里清理。

插件 id 里的 / 会被替换成 . 作为目录名。比如 com/example/friend.xxx → com.example.friend.xxx.moodfriend。

final installer = PluginInstaller(repository);
final result = await installer.install(
plugin,
onProgress: (p) {
setState(() => _progress = p);
},
);
if (result.success) {
print('安装成功: ${result.installedIds}');
ref.invalidate(pluginManagerProvider); // 触发重新扫描
} else {
print('安装失败: ${result.error}');
}

更新就是重新 install 同一个插件——copyDirectory 会先删旧目录再复制新的,等于覆盖安装。UI 上判断“更新”还是“安装”只是看 pluginManager.pluginById(id) 是否已存在,底层 install 逻辑一样。

final isInstalled = manager.pluginById(plugin.id) != null;
final result = await installer.install(plugin);
if (result.success) {
SnackBar(content: Text(isInstalled ? '更新成功' : '安装成功'));
}

Pack 走 installPack(tmpDir)(来自 pack_installer.dart):

  1. 读整合包 manifest,解析 plugins.friend 和 plugins.application
  2. 对每个引用的子插件,copyDirectory 复制到 plugins root
  3. 整合包目录本身也复制到 plugins root
  4. 返回 PackInstallResult.ok(installedPluginIds)

PluginInstaller 把 PackInstallResult 转成 PluginInstallResult。如果 installPack 失败,返回 PluginInstallResult.fail。

错误 原因
package is not a valid zip 下载的文件不是合法 ZIP
manifest.json not found in package ZIP 根目录没有 manifest.json
manifest.json is not a JSON object manifest.json 不是 JSON 对象
manifest.json invalid: ... JSON 解析失败
manifest id (...) does not match market id (...) 包内 manifest 的 id 和市场 meta 的 id 不一致
manifest type (...) does not match market type (...) type 不一致
unsafe entry path in package: ... ZIP 里有绝对路径或 ..(zip-slip 攻击)
pack install failed: ... 整合包展开失败

PluginInstaller 不直接调 PluginManager 的任何方法。它只:

  1. 用 pluginsRootDir() 拿到插件根目录路径(来自 plugin_paths.dart)
  2. 把文件复制进去

安装完后由调用方(UI / Provider 层)调 ref.invalidate(pluginManagerProvider),这会触发 PluginManager.refresh() 重新扫描目录,新插件就出现在 scan.ok 列表里。

这个设计让 PluginInstaller 不依赖 PluginManager 类,可以独立测试。