一、功能概述
本实现为应用列表页面( 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(', ')}`)
}