安装本地插件逻辑

一、功能概述

本实现为应用列表页面( app\components\apps\index.tsx )添加了本地插件自动安装功能,在组件加载时自动检测并安装 public/插件 文件夹下的 .difypkg 插件包。

二、实现架构

2.1 整体流程

组件加载 → 获取本地插件文件 → 上传服务器 → 获取 unique_identifier → 安装插件

2.2 核心技术栈

技术/工具 用途
React Hooks 管理插件安装状态和副作用
uploadFile 上传插件文件到服务器
useInstallPackageFromLocal 安装本地插件的 mutation 钩子
Fetch API 获取本地插件文件

三、代码实现详解

3.1 导入依赖

import { useInstallPackageFromMarketPlace, useInstalledLatestVersion, useInvalidateInstalledPluginList, useUpdatePackageFromMarketPlace, useInstallPackageFromLocal } from '@/service/use-plugins'
import { uploadFile } from '@/service/plugins'

关键点说明:

  • useInstallPackageFromLocal :用于调用本地插件安装接口
  • uploadFile :用于上传插件文件( .difypkg )

3.2 初始化钩子

const { mutateAsync: installPackageFromLocal } = useInstallPackageFromLocal()

此钩子封装了 POST 请求到 /workspaces/current/plugin/install/pkg 接口。

3.3 核心安装逻辑

// 本地插件列表
const localPluginsToInstall = [
  '/插件/skill_agent_0.0.1.difypkg',
  '/插件/alipay-alipay_plugin_0.0.1.difypkg',
]

for (const pluginPath of localPluginsToInstall) {
  // 1. 获取本地文件
  const fileResponse = await fetch(pluginPath)
  const blob = await fileResponse.blob()
  const file = new File([blob], fileName, { type: 'application/octet-stream' })

  // 2. 上传文件(关键:成功响应在 catch 中)
  let uploadResult: any
  try {
    await uploadFile(file, false)
  } catch (e: any) {
    if (e.response?.message) {
      throw new Error(e.response.message)
    }
    uploadResult = e.response
  }

  // 3. 安装插件
  const uniqueIdentifier = uploadResult.unique_identifier
  const installResult = await installPackageFromLocal(uniqueIdentifier)
}

四、关键技术点解析

4.1 uploadFile 响应处理机制

这是本实现中 最关键 的技术点:

try {
  await uploadFile(file, false)
} catch (e: any) {
  if (e.response?.message) {
    throw new Error(e.response.message)  // 真正的错误
  }
  uploadResult = e.response  // 成功响应!
}

为什么成功响应会在 catch 中?

查看项目中 service/plugins.ts 的实现:

export const uploadFile = async (file: File, isBundle: boolean) => {
  const formData = new FormData()
  formData.append(isBundle ? 'bundle' : 'pkg', file)
  return upload({
    xhr: new XMLHttpRequest(),
    data: formData,
  }, false, `/workspaces/current/plugin/upload/${isBundle ? 'bundle' : 'pkg'}`)
}

底层的 upload 函数使用了特殊的 XHR 处理方式,将所有响应(包括成功响应)都通过 reject 抛出,这是项目的历史设计模式。

4.2 接口调用流程

步骤 接口 方法 作用 1 /插件/xxx.difypkg GET 获取本地插件文件 2 /workspaces/current/plugin/upload/pkg POST 上传插件文件 3 /workspaces/current/plugin/install/pkg POST 安装插件

4.3 数据流转

本地文件 (.difypkg) 
  → File 对象 
  → uploadFile → 服务器响应 { unique_identifier, manifest }
  → installPackageFromLocal(unique_identifier) → 安装成功

五、与现有实现的对比

5.1 参考实现:install-from-local-package 组件

项目中已有的本地插件安装组件位于: app/components/plugins/install-plugin/install-from-local-package/

其核心流程:

// uploading.tsx
const handleUpload = async () => {
  try {
    await uploadFile(file, isBundle)
  } catch (e: any) {
    if (e.response?.message) {
      onFailed(e.response?.message)
    } else {
      const res = e.response
      onPackageUploaded({
        uniqueIdentifier: res.unique_identifier,
        manifest: res.manifest,
      })
    }
  }
}

// install.tsx
const handleInstall = async () => {
  const { all_installed, task_id } = await installPackageFromLocal
  (uniqueIdentifier)
  // ...
}

5.2 本实现的差异

对比项 现有组件 本实现 触发方式 用户手动选择文件 组件加载时自动触发 UI 交互 完整的模态框流程 后台静默安装 错误处理 显示错误提示给用户 仅控制台日志 适用场景 用户手动安装 初始化自动部署

六、代码优化建议

6.1 错误处理增强

当前实现仅使用 console.error ,建议增加更完善的错误处理:

try {
  // 安装逻辑
} catch (error) {
  console.error(`本地插件 ${pluginPath} 安装失败:`, error)
  // 可选:上报错误监控系统
  // reportError(error, { pluginPath })
}

6.2 去重安装检测

在安装前检查插件是否已安装:


const isPluginInstalled = await checkPluginInstallation(uniqueIdentifier)
if (isPluginInstalled) {
  console.log(`插件 ${uniqueIdentifier} 已安装,跳过`)
  continue
}

七、实现代码

// 安装本地插件(从 public/插件 文件夹)
      const localPluginsToInstall = [
        '/插件/jkg_skill_agent_0.0.1.difypkg',
        '/插件/alipay-alipay_plugin_0.0.1.difypkg',
      ]

      console.log('开始并行安装本地插件...')
      
      // 检查插件是否已安装的辅助函数
      const checkPluginInstalled = async (pluginId: string): Promise<boolean> => {
        try {
          const response = await fetch('/api/workspaces/current/plugin/list/installations/ids', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({
              plugin_ids: [pluginId],
            }),
          })
          
          if (!response.ok) {
            console.warn(`检查插件安装状态失败: ${pluginId}`, response.statusText)
            return false
          }
          
          const result = await response.json()
          const plugins = result?.plugins || []
          return plugins.length > 0 && plugins[0]?.installedVersion !== undefined
        } catch (error) {
          console.warn(`检查插件安装状态异常: ${pluginId}`, error)
          return false
        }
      }
      
      // 并行安装所有本地插件
      const installPromises = localPluginsToInstall.map(async (pluginPath) => {
        const fileName = pluginPath.split('/').pop() || 'plugin.difypkg'
        
        try {
          console.log(`=== 安装本地插件: ${fileName} ===`)
          
          // 第一步:获取本地插件文件
          console.log(`1. 获取本地插件文件: ${pluginPath}`)
          const fileResponse = await fetch(pluginPath)
          if (!fileResponse.ok) {
            console.error(`获取本地插件文件失败: ${pluginPath}`, fileResponse.statusText)
            return { pluginPath, fileName, success: false, error: `获取文件失败: ${fileResponse.statusText}` }
          }
          const blob = await fileResponse.blob()
          const file = new File([blob], fileName, { type: 'application/octet-stream' })

          // 第二步:上传插件文件(注意:uploadFile 成功响应在 catch 分支中)
          console.log(`2. 上传插件文件: ${fileName}`)
          let uploadResult: any
          try {
            await uploadFile(file, false)
          } catch (e: any) {
            // uploadFile 成功时,响应会在 catch 中返回
            if (e.response?.message) {
              throw new Error(e.response.message)
            }
            uploadResult = e.response
          }

          if (!uploadResult) {
            console.error(`上传插件文件失败: 未获取到响应`)
            return { pluginPath, fileName, success: false, error: '上传失败: 未获取到响应' }
          }

          const uniqueIdentifier = uploadResult.unique_identifier
          const manifest = uploadResult.manifest
          
          if (!uniqueIdentifier) {
            console.error(`上传插件文件失败: 缺少 unique_identifier`)
            return { pluginPath, fileName, success: false, error: '上传失败: 缺少 unique_identifier' }
          }
          console.log(`插件文件上传成功,unique_identifier: ${uniqueIdentifier}`)

          // 第三步:去重检测 - 检查插件是否已安装
          if (manifest && manifest.author && manifest.name) {
            const pluginId = `${manifest.author}/${manifest.name}`
            console.log(`3. 检查插件是否已安装: ${pluginId}`)
            
            const isInstalled = await checkPluginInstalled(pluginId)
            if (isInstalled) {
              console.log(`插件 ${pluginId} 已安装,跳过安装`)
              return { pluginPath, fileName, success: true, skipped: true, message: '插件已安装,跳过' }
            }
          }

          // 第四步:安装插件
          console.log(`4. 安装插件: ${uniqueIdentifier}`)
          const installResult = await installPackageFromLocal(uniqueIdentifier)
          console.log(`本地插件 ${fileName} 安装成功:`, installResult)
          
          return { pluginPath, fileName, success: true, result: installResult }
        } catch (error) {
          console.error(`本地插件 ${pluginPath} 安装失败:`, error)
          // return { pluginPath, fileName, success: false, error }
        }
      })

      // 等待所有插件安装完成
      const installResults = await Promise.all(installPromises)
      
      // 输出安装结果统计
      const successCount = installResults.filter(r => r.success && !r.skipped).length
      const skippedCount = installResults.filter(r => r.success && r.skipped).length
      const failCount = installResults.filter(r => !r.success).length
      console.log(`本地插件安装完成: 成功 ${successCount} 个, 跳过 ${skippedCount} 个, 失败 ${failCount} 个`)
      if (failCount > 0) {
        const failedPlugins = installResults.filter(r => !r.success).map(r => r.fileName)
        console.log(`安装失败的插件: ${failedPlugins.join(', ')}`)
      }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容