PluginInstaller
PluginInstaller
Section titled “PluginInstaller”PluginInstaller 把市场里的插件装到用户设备上。流程:下载 ZIP → 解压 → 校验 manifest → 复制到 plugins root → 返回结果。安装完成后由调用方调 ref.invalidate(pluginManagerProvider) 触发 PluginManager.refresh()。
文件:lib/core/plugin/plugin_installer.dart
PluginInstallResult
Section titled “PluginInstallResult”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);PluginInstaller
Section titled “PluginInstaller”class PluginInstaller { final MarketRepository repository;
PluginInstaller(this.repository);}install
Section titled “install”Future<PluginInstallResult> install( MarketPlugin plugin, { void Function(double progress)? onProgress,})安装一个市场插件。onProgress 在下载阶段收到 0.0–1.0 的进度(解压/复制阶段不调)。
- 下载:
repository.downloadPackage(dir, id, onProgress)——走磁盘缓存(永久),未命中才流式下载 - 解压:
ZipDecoder().decodeBytes(packageBytes),逐条目解压到临时目录- 拒绝绝对路径(
/开头)和..(防 zip-slip 攻击) - 只处理文件条目(跳过目录条目,创建父目录即可)
- 拒绝绝对路径(
- 校验 manifest:读临时目录的
manifest.json- 必须存在且是 JSON 对象
id必须和MarketPlugin.id一致type必须和MarketPlugin.type一致
- 复制到 plugins root:
- Friend / Application:
copyDirectory(tmpDir, <pluginsRoot>/<safeId>.mood*) - Pack:
installPack(tmpDir)——展开各子插件目录 + 整合包目录
- Friend / Application:
- 返回
PluginInstallResult.ok(installedIds)
任何步骤失败返回 PluginInstallResult.fail(error),不抛异常。临时目录在 finally 里清理。
safeId
Section titled “safeId”插件 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 安装细节
Section titled “Pack 安装细节”Pack 走 installPack(tmpDir)(来自 pack_installer.dart):
- 读整合包 manifest,解析
plugins.friend和plugins.application - 对每个引用的子插件,
copyDirectory复制到 plugins root - 整合包目录本身也复制到 plugins root
- 返回
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: ... |
整合包展开失败 |
和 PluginManager 的关系
Section titled “和 PluginManager 的关系”PluginInstaller 不直接调 PluginManager 的任何方法。它只:
- 用
pluginsRootDir()拿到插件根目录路径(来自plugin_paths.dart) - 把文件复制进去
安装完后由调用方(UI / Provider 层)调 ref.invalidate(pluginManagerProvider),这会触发 PluginManager.refresh() 重新扫描目录,新插件就出现在 scan.ok 列表里。
这个设计让 PluginInstaller 不依赖 PluginManager 类,可以独立测试。