Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

use-plugins.ts 18KB

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