You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

use-plugins.ts 19KB

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