安裝本地插件邏輯

一、功能概述

本實現(xiàn)為應(yīng)用列表頁面( app\components\apps\index.tsx )添加了本地插件自動安裝功能,在組件加載時自動檢測并安裝 public/插件 文件夾下的 .difypkg 插件包。

二、實現(xiàn)架構(gòu)

2.1 整體流程

組件加載 → 獲取本地插件文件 → 上傳服務(wù)器 → 獲取 unique_identifier → 安裝插件

2.2 核心技術(shù)棧

技術(shù)/工具 用途
React Hooks 管理插件安裝狀態(tài)和副作用
uploadFile 上傳插件文件到服務(wù)器
useInstallPackageFromLocal 安裝本地插件的 mutation 鉤子
Fetch API 獲取本地插件文件

三、代碼實現(xiàn)詳解

3.1 導(dǎo)入依賴

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

關(guān)鍵點(diǎn)說明:

  • useInstallPackageFromLocal :用于調(diào)用本地插件安裝接口
  • 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. 上傳文件(關(guān)鍵:成功響應(yīng)在 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)
}

四、關(guān)鍵技術(shù)點(diǎn)解析

4.1 uploadFile 響應(yīng)處理機(jī)制

這是本實現(xiàn)中 最關(guān)鍵 的技術(shù)點(diǎn):

try {
  await uploadFile(file, false)
} catch (e: any) {
  if (e.response?.message) {
    throw new Error(e.response.message)  // 真正的錯誤
  }
  uploadResult = e.response  // 成功響應(yīng)!
}

為什么成功響應(yīng)會在 catch 中?

查看項目中 service/plugins.ts 的實現(xiàn):

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 函數(shù)使用了特殊的 XHR 處理方式,將所有響應(yīng)(包括成功響應(yīng))都通過 reject 拋出,這是項目的歷史設(shè)計模式。

4.2 接口調(diào)用流程

步驟 接口 方法 作用 1 /插件/xxx.difypkg GET 獲取本地插件文件 2 /workspaces/current/plugin/upload/pkg POST 上傳插件文件 3 /workspaces/current/plugin/install/pkg POST 安裝插件

4.3 數(shù)據(jù)流轉(zhuǎn)

本地文件 (.difypkg) 
  → File 對象 
  → uploadFile → 服務(wù)器響應(yīng) { unique_identifier, manifest }
  → installPackageFromLocal(unique_identifier) → 安裝成功

五、與現(xiàn)有實現(xiàn)的對比

5.1 參考實現(xiàn):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 本實現(xiàn)的差異

對比項 現(xiàn)有組件 本實現(xiàn) 觸發(fā)方式 用戶手動選擇文件 組件加載時自動觸發(fā) UI 交互 完整的模態(tài)框流程 后臺靜默安裝 錯誤處理 顯示錯誤提示給用戶 僅控制臺日志 適用場景 用戶手動安裝 初始化自動部署

六、代碼優(yōu)化建議

6.1 錯誤處理增強(qiáng)

當(dāng)前實現(xiàn)僅使用 console.error ,建議增加更完善的錯誤處理:

try {
  // 安裝邏輯
} catch (error) {
  console.error(`本地插件 ${pluginPath} 安裝失敗:`, error)
  // 可選:上報錯誤監(jiān)控系統(tǒng)
  // reportError(error, { pluginPath })
}

6.2 去重安裝檢測

在安裝前檢查插件是否已安裝:


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

七、實現(xiàn)代碼

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

      console.log('開始并行安裝本地插件...')
      
      // 檢查插件是否已安裝的輔助函數(shù)
      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(`檢查插件安裝狀態(tài)失敗: ${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(`檢查插件安裝狀態(tài)異常: ${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 成功響應(yīng)在 catch 分支中)
          console.log(`2. 上傳插件文件: ${fileName}`)
          let uploadResult: any
          try {
            await uploadFile(file, false)
          } catch (e: any) {
            // uploadFile 成功時,響應(yīng)會在 catch 中返回
            if (e.response?.message) {
              throw new Error(e.response.message)
            }
            uploadResult = e.response
          }

          if (!uploadResult) {
            console.error(`上傳插件文件失敗: 未獲取到響應(yīng)`)
            return { pluginPath, fileName, success: false, error: '上傳失敗: 未獲取到響應(yīng)' }
          }

          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)
      
      // 輸出安裝結(jié)果統(tǒng)計
      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(', ')}`)
      }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容