use-plugins.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import { useCallback, useEffect } from 'react'
  2. import type {
  3. ModelProvider,
  4. } from '@/app/components/header/account-setting/model-provider-page/declarations'
  5. import { fetchModelProviderModelList } from '@/service/common'
  6. import { fetchPluginInfoFromMarketPlace } from '@/service/plugins'
  7. import type {
  8. DebugInfo as DebugInfoTypes,
  9. Dependency,
  10. GitHubItemAndMarketPlaceDependency,
  11. InstallPackageResponse,
  12. InstalledPluginListResponse,
  13. PackageDependency,
  14. Permissions,
  15. Plugin,
  16. PluginDetail,
  17. PluginInfoFromMarketPlace,
  18. PluginTask,
  19. PluginType,
  20. PluginsFromMarketplaceByInfoResponse,
  21. PluginsFromMarketplaceResponse,
  22. VersionInfo,
  23. VersionListResponse,
  24. uploadGitHubResponse,
  25. } from '@/app/components/plugins/types'
  26. import { TaskStatus } from '@/app/components/plugins/types'
  27. import { PluginType as PluginTypeEnum } from '@/app/components/plugins/types'
  28. import type {
  29. PluginsSearchParams,
  30. } from '@/app/components/plugins/marketplace/types'
  31. import { get, getMarketplace, post, postMarketplace } from './base'
  32. import type { MutateOptions, QueryOptions } from '@tanstack/react-query'
  33. import {
  34. useMutation,
  35. useQuery,
  36. useQueryClient,
  37. } from '@tanstack/react-query'
  38. import { useInvalidateAllBuiltInTools } from './use-tools'
  39. import usePermission from '@/app/components/plugins/plugin-page/use-permission'
  40. import { uninstallPlugin } from '@/service/plugins'
  41. import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
  42. import { cloneDeep } from 'lodash-es'
  43. const NAME_SPACE = 'plugins'
  44. const useInstalledPluginListKey = [NAME_SPACE, 'installedPluginList']
  45. export const useCheckInstalled = ({
  46. pluginIds,
  47. enabled,
  48. }: {
  49. pluginIds: string[],
  50. enabled: boolean
  51. }) => {
  52. return useQuery<{ plugins: PluginDetail[] }>({
  53. queryKey: [NAME_SPACE, 'checkInstalled', pluginIds],
  54. queryFn: () => post<{ plugins: PluginDetail[] }>('/workspaces/current/plugin/list/installations/ids', {
  55. body: {
  56. plugin_ids: pluginIds,
  57. },
  58. }),
  59. enabled,
  60. staleTime: 0, // always fresh
  61. })
  62. }
  63. export const useInstalledPluginList = (disable?: boolean) => {
  64. return useQuery<InstalledPluginListResponse>({
  65. queryKey: useInstalledPluginListKey,
  66. queryFn: () => get<InstalledPluginListResponse>('/workspaces/current/plugin/list'),
  67. enabled: !disable,
  68. initialData: !disable ? undefined : { plugins: [] },
  69. })
  70. }
  71. export const useInvalidateInstalledPluginList = () => {
  72. const queryClient = useQueryClient()
  73. const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
  74. return () => {
  75. queryClient.invalidateQueries(
  76. {
  77. queryKey: useInstalledPluginListKey,
  78. })
  79. invalidateAllBuiltInTools()
  80. }
  81. }
  82. export const useInstallPackageFromMarketPlace = (options?: MutateOptions<InstallPackageResponse, Error, string>) => {
  83. return useMutation({
  84. ...options,
  85. mutationFn: (uniqueIdentifier: string) => {
  86. return post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', { body: { plugin_unique_identifiers: [uniqueIdentifier] } })
  87. },
  88. })
  89. }
  90. export const useUpdatePackageFromMarketPlace = (options?: MutateOptions<InstallPackageResponse, Error, object>) => {
  91. return useMutation({
  92. ...options,
  93. mutationFn: (body: object) => {
  94. return post<InstallPackageResponse>('/workspaces/current/plugin/upgrade/marketplace', {
  95. body,
  96. })
  97. },
  98. })
  99. }
  100. export const useVersionListOfPlugin = (pluginID: string) => {
  101. return useQuery<{ data: VersionListResponse }>({
  102. enabled: !!pluginID,
  103. queryKey: [NAME_SPACE, 'versions', pluginID],
  104. queryFn: () => getMarketplace<{ data: VersionListResponse }>(`/plugins/${pluginID}/versions`, { params: { page: 1, page_size: 100 } }),
  105. })
  106. }
  107. export const useInvalidateVersionListOfPlugin = () => {
  108. const queryClient = useQueryClient()
  109. return (pluginID: string) => {
  110. queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'versions', pluginID] })
  111. }
  112. }
  113. export const useInstallPackageFromLocal = () => {
  114. return useMutation({
  115. mutationFn: (uniqueIdentifier: string) => {
  116. return post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
  117. body: { plugin_unique_identifiers: [uniqueIdentifier] },
  118. })
  119. },
  120. })
  121. }
  122. export const useInstallPackageFromGitHub = () => {
  123. return useMutation({
  124. mutationFn: ({ repoUrl, selectedVersion, selectedPackage, uniqueIdentifier }: {
  125. repoUrl: string
  126. selectedVersion: string
  127. selectedPackage: string
  128. uniqueIdentifier: string
  129. }) => {
  130. return post<InstallPackageResponse>('/workspaces/current/plugin/install/github', {
  131. body: {
  132. repo: repoUrl,
  133. version: selectedVersion,
  134. package: selectedPackage,
  135. plugin_unique_identifier: uniqueIdentifier,
  136. },
  137. })
  138. },
  139. })
  140. }
  141. export const useUploadGitHub = (payload: {
  142. repo: string
  143. version: string
  144. package: string
  145. }) => {
  146. return useQuery({
  147. queryKey: [NAME_SPACE, 'uploadGitHub', payload],
  148. queryFn: () => post<uploadGitHubResponse>('/workspaces/current/plugin/upload/github', {
  149. body: payload,
  150. }),
  151. retry: 0,
  152. })
  153. }
  154. export const useInstallOrUpdate = ({
  155. onSuccess,
  156. }: {
  157. onSuccess?: (res: { success: boolean }[]) => void
  158. }) => {
  159. const { mutateAsync: updatePackageFromMarketPlace } = useUpdatePackageFromMarketPlace()
  160. return useMutation({
  161. mutationFn: (data: {
  162. payload: Dependency[],
  163. plugin: Plugin[],
  164. installedInfo: Record<string, VersionInfo>
  165. }) => {
  166. const { payload, plugin, installedInfo } = data
  167. return Promise.all(payload.map(async (item, i) => {
  168. try {
  169. const orgAndName = `${plugin[i]?.org || plugin[i]?.author}/${plugin[i]?.name}`
  170. const installedPayload = installedInfo[orgAndName]
  171. const isInstalled = !!installedPayload
  172. let uniqueIdentifier = ''
  173. if (item.type === 'github') {
  174. const data = item as GitHubItemAndMarketPlaceDependency
  175. // From local bundle don't have data.value.github_plugin_unique_identifier
  176. uniqueIdentifier = data.value.github_plugin_unique_identifier!
  177. if (!uniqueIdentifier) {
  178. const { unique_identifier } = await post<uploadGitHubResponse>('/workspaces/current/plugin/upload/github', {
  179. body: {
  180. repo: data.value.repo!,
  181. version: data.value.release! || data.value.version!,
  182. package: data.value.packages! || data.value.package!,
  183. },
  184. })
  185. uniqueIdentifier = data.value.github_plugin_unique_identifier! || unique_identifier
  186. // has the same version, but not installed
  187. if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
  188. return {
  189. success: true,
  190. }
  191. }
  192. }
  193. if (!isInstalled) {
  194. await post<InstallPackageResponse>('/workspaces/current/plugin/install/github', {
  195. body: {
  196. repo: data.value.repo!,
  197. version: data.value.release! || data.value.version!,
  198. package: data.value.packages! || data.value.package!,
  199. plugin_unique_identifier: uniqueIdentifier,
  200. },
  201. })
  202. }
  203. }
  204. if (item.type === 'marketplace') {
  205. const data = item as GitHubItemAndMarketPlaceDependency
  206. uniqueIdentifier = data.value.marketplace_plugin_unique_identifier! || plugin[i]?.plugin_id
  207. if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
  208. return {
  209. success: true,
  210. }
  211. }
  212. if (!isInstalled) {
  213. await post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', {
  214. body: {
  215. plugin_unique_identifiers: [uniqueIdentifier],
  216. },
  217. })
  218. }
  219. }
  220. if (item.type === 'package') {
  221. const data = item as PackageDependency
  222. uniqueIdentifier = data.value.unique_identifier
  223. if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
  224. return {
  225. success: true,
  226. }
  227. }
  228. if (!isInstalled) {
  229. await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
  230. body: {
  231. plugin_unique_identifiers: [uniqueIdentifier],
  232. },
  233. })
  234. }
  235. }
  236. if (isInstalled) {
  237. if (item.type === 'package') {
  238. await uninstallPlugin(installedPayload.installedId)
  239. await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
  240. body: {
  241. plugin_unique_identifiers: [uniqueIdentifier],
  242. },
  243. })
  244. }
  245. else {
  246. await updatePackageFromMarketPlace({
  247. original_plugin_unique_identifier: installedPayload?.uniqueIdentifier,
  248. new_plugin_unique_identifier: uniqueIdentifier,
  249. })
  250. }
  251. }
  252. return ({ success: true })
  253. }
  254. // eslint-disable-next-line unused-imports/no-unused-vars
  255. catch (e) {
  256. return Promise.resolve({ success: false })
  257. }
  258. }))
  259. },
  260. onSuccess,
  261. })
  262. }
  263. export const useDebugKey = () => {
  264. return useQuery({
  265. queryKey: [NAME_SPACE, 'debugKey'],
  266. queryFn: () => get<DebugInfoTypes>('/workspaces/current/plugin/debugging-key'),
  267. })
  268. }
  269. const usePermissionsKey = [NAME_SPACE, 'permissions']
  270. export const usePermissions = () => {
  271. return useQuery({
  272. queryKey: usePermissionsKey,
  273. queryFn: () => get<Permissions>('/workspaces/current/plugin/permission/fetch'),
  274. })
  275. }
  276. export const useInvalidatePermissions = () => {
  277. const queryClient = useQueryClient()
  278. return () => {
  279. queryClient.invalidateQueries(
  280. {
  281. queryKey: usePermissionsKey,
  282. })
  283. }
  284. }
  285. export const useMutationPermissions = ({
  286. onSuccess,
  287. }: {
  288. onSuccess?: () => void
  289. }) => {
  290. return useMutation({
  291. mutationFn: (payload: Permissions) => {
  292. return post('/workspaces/current/plugin/permission/change', { body: payload })
  293. },
  294. onSuccess,
  295. })
  296. }
  297. export const useMutationPluginsFromMarketplace = () => {
  298. return useMutation({
  299. mutationFn: (pluginsSearchParams: PluginsSearchParams) => {
  300. const {
  301. query,
  302. sortBy,
  303. sortOrder,
  304. category,
  305. tags,
  306. exclude,
  307. type,
  308. page = 1,
  309. pageSize = 40,
  310. } = pluginsSearchParams
  311. const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
  312. return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
  313. body: {
  314. page,
  315. page_size: pageSize,
  316. query,
  317. sort_by: sortBy,
  318. sort_order: sortOrder,
  319. category: category !== 'all' ? category : '',
  320. tags,
  321. exclude,
  322. type,
  323. },
  324. })
  325. },
  326. })
  327. }
  328. export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[], options?: QueryOptions<{ data: PluginsFromMarketplaceResponse }>) => {
  329. return useQuery({
  330. ...options,
  331. queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByIds', unique_identifiers],
  332. queryFn: () => postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/identifier/batch', {
  333. body: {
  334. unique_identifiers,
  335. },
  336. }),
  337. enabled: unique_identifiers?.filter(i => !!i).length > 0,
  338. retry: 0,
  339. })
  340. }
  341. export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[]) => {
  342. return useQuery({
  343. queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByInfo', infos],
  344. queryFn: () => postMarketplace<{ data: PluginsFromMarketplaceByInfoResponse }>('/plugins/versions/batch', {
  345. body: {
  346. plugin_tuples: infos.map(info => ({
  347. org: info.organization,
  348. name: info.plugin,
  349. version: info.version,
  350. })),
  351. },
  352. }),
  353. enabled: infos?.filter(i => !!i).length > 0,
  354. retry: 0,
  355. })
  356. }
  357. const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
  358. export const usePluginTaskList = (category?: PluginType) => {
  359. const {
  360. canManagement,
  361. } = usePermission()
  362. const { refreshPluginList } = useRefreshPluginList()
  363. const {
  364. data,
  365. isFetched,
  366. isRefetching,
  367. refetch,
  368. ...rest
  369. } = useQuery({
  370. enabled: canManagement,
  371. queryKey: usePluginTaskListKey,
  372. queryFn: () => get<{ tasks: PluginTask[] }>('/workspaces/current/plugin/tasks?page=1&page_size=100'),
  373. refetchInterval: (lastQuery) => {
  374. const lastData = lastQuery.state.data
  375. const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
  376. return taskDone ? false : 5000
  377. },
  378. })
  379. useEffect(() => {
  380. // After first fetch, refresh plugin list each time all tasks are done
  381. if (!isRefetching) {
  382. const lastData = cloneDeep(data)
  383. const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
  384. const taskAllFailed = lastData?.tasks.every(task => task.status === TaskStatus.failed)
  385. if (taskDone) {
  386. if (lastData?.tasks.length && !taskAllFailed)
  387. refreshPluginList(category ? { category } as any : undefined, !category)
  388. }
  389. }
  390. // eslint-disable-next-line react-hooks/exhaustive-deps
  391. }, [isRefetching])
  392. const handleRefetch = useCallback(() => {
  393. refetch()
  394. }, [refetch])
  395. return {
  396. data,
  397. pluginTasks: data?.tasks || [],
  398. isFetched,
  399. handleRefetch,
  400. ...rest,
  401. }
  402. }
  403. export const useMutationClearTaskPlugin = () => {
  404. return useMutation({
  405. mutationFn: ({ taskId, pluginId }: { taskId: string; pluginId: string }) => {
  406. return post<{ success: boolean }>(`/workspaces/current/plugin/tasks/${taskId}/delete/${pluginId}`)
  407. },
  408. })
  409. }
  410. export const useMutationClearAllTaskPlugin = () => {
  411. return useMutation({
  412. mutationFn: () => {
  413. return post<{ success: boolean }>('/workspaces/current/plugin/tasks/delete_all')
  414. },
  415. })
  416. }
  417. export const usePluginManifestInfo = (pluginUID: string) => {
  418. return useQuery({
  419. enabled: !!pluginUID,
  420. queryKey: [[NAME_SPACE, 'manifest', pluginUID]],
  421. queryFn: () => getMarketplace<{ data: { plugin: PluginInfoFromMarketPlace, version: { version: string } } }>(`/plugins/${pluginUID}`),
  422. retry: 0,
  423. })
  424. }
  425. export const useDownloadPlugin = (info: { organization: string; pluginName: string; version: string }, needDownload: boolean) => {
  426. return useQuery({
  427. queryKey: [NAME_SPACE, 'downloadPlugin', info],
  428. queryFn: () => getMarketplace<Blob>(`/plugins/${info.organization}/${info.pluginName}/${info.version}/download`),
  429. enabled: needDownload,
  430. retry: 0,
  431. })
  432. }
  433. export const useMutationCheckDependencies = () => {
  434. return useMutation({
  435. mutationFn: (appId: string) => {
  436. return get<{ leaked_dependencies: Dependency[] }>(`/apps/imports/${appId}/check-dependencies`)
  437. },
  438. })
  439. }
  440. export const useModelInList = (currentProvider?: ModelProvider, modelId?: string) => {
  441. return useQuery({
  442. queryKey: ['modelInList', currentProvider?.provider, modelId],
  443. queryFn: async () => {
  444. if (!modelId || !currentProvider) return false
  445. try {
  446. const modelsData = await fetchModelProviderModelList(`/workspaces/current/model-providers/${currentProvider?.provider}/models`)
  447. return !!modelId && !!modelsData.data.find(item => item.model === modelId)
  448. }
  449. catch (error) {
  450. return false
  451. }
  452. },
  453. enabled: !!modelId && !!currentProvider,
  454. })
  455. }
  456. export const usePluginInfo = (providerName?: string) => {
  457. return useQuery({
  458. queryKey: ['pluginInfo', providerName],
  459. queryFn: async () => {
  460. if (!providerName) return null
  461. const parts = providerName.split('/')
  462. const org = parts[0]
  463. const name = parts[1]
  464. try {
  465. const response = await fetchPluginInfoFromMarketPlace({ org, name })
  466. return response.data.plugin.category === PluginTypeEnum.model ? response.data.plugin : null
  467. }
  468. catch (error) {
  469. return null
  470. }
  471. },
  472. enabled: !!providerName,
  473. })
  474. }