一、功能概述
本實現(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(', ')}`)
}