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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  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, useInvalidateRAGRecommendedPlugins } 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. const invalidateRAGRecommendedPlugins = useInvalidateRAGRecommendedPlugins()
  129. return () => {
  130. queryClient.invalidateQueries(
  131. {
  132. queryKey: useInstalledPluginListKey,
  133. })
  134. invalidateAllBuiltInTools()
  135. invalidateRAGRecommendedPlugins()
  136. }
  137. }
  138. export const useInstallPackageFromMarketPlace = (options?: MutateOptions<InstallPackageResponse, Error, string>) => {
  139. return useMutation({
  140. ...options,
  141. mutationFn: (uniqueIdentifier: string) => {
  142. return post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', { body: { plugin_unique_identifiers: [uniqueIdentifier] } })
  143. },
  144. })
  145. }
  146. export const useUpdatePackageFromMarketPlace = (options?: MutateOptions<InstallPackageResponse, Error, object>) => {
  147. return useMutation({
  148. ...options,
  149. mutationFn: (body: object) => {
  150. return post<InstallPackageResponse>('/workspaces/current/plugin/upgrade/marketplace', {
  151. body,
  152. })
  153. },
  154. })
  155. }
  156. export const usePluginDeclarationFromMarketPlace = (pluginUniqueIdentifier: string) => {
  157. return useQuery({
  158. queryKey: [NAME_SPACE, 'pluginDeclaration', pluginUniqueIdentifier],
  159. queryFn: () => get<{ manifest: PluginDeclaration }>('/workspaces/current/plugin/marketplace/pkg', { params: { plugin_unique_identifier: pluginUniqueIdentifier } }),
  160. enabled: !!pluginUniqueIdentifier,
  161. })
  162. }
  163. export const useVersionListOfPlugin = (pluginID: string) => {
  164. return useQuery<{ data: VersionListResponse }>({
  165. enabled: !!pluginID,
  166. queryKey: [NAME_SPACE, 'versions', pluginID],
  167. queryFn: () => getMarketplace<{ data: VersionListResponse }>(`/plugins/${pluginID}/versions`, { params: { page: 1, page_size: 100 } }),
  168. })
  169. }
  170. export const useInvalidateVersionListOfPlugin = () => {
  171. const queryClient = useQueryClient()
  172. return (pluginID: string) => {
  173. queryClient.invalidateQueries({ queryKey: [NAME_SPACE, 'versions', pluginID] })
  174. }
  175. }
  176. export const useInstallPackageFromLocal = () => {
  177. return useMutation({
  178. mutationFn: (uniqueIdentifier: string) => {
  179. return post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
  180. body: { plugin_unique_identifiers: [uniqueIdentifier] },
  181. })
  182. },
  183. })
  184. }
  185. export const useInstallPackageFromGitHub = () => {
  186. return useMutation({
  187. mutationFn: ({ repoUrl, selectedVersion, selectedPackage, uniqueIdentifier }: {
  188. repoUrl: string
  189. selectedVersion: string
  190. selectedPackage: string
  191. uniqueIdentifier: string
  192. }) => {
  193. return post<InstallPackageResponse>('/workspaces/current/plugin/install/github', {
  194. body: {
  195. repo: repoUrl,
  196. version: selectedVersion,
  197. package: selectedPackage,
  198. plugin_unique_identifier: uniqueIdentifier,
  199. },
  200. })
  201. },
  202. })
  203. }
  204. export const useUploadGitHub = (payload: {
  205. repo: string
  206. version: string
  207. package: string
  208. }) => {
  209. return useQuery({
  210. queryKey: [NAME_SPACE, 'uploadGitHub', payload],
  211. queryFn: () => post<uploadGitHubResponse>('/workspaces/current/plugin/upload/github', {
  212. body: payload,
  213. }),
  214. retry: 0,
  215. })
  216. }
  217. export const useInstallOrUpdate = ({
  218. onSuccess,
  219. }: {
  220. onSuccess?: (res: { success: boolean }[]) => void
  221. }) => {
  222. const { mutateAsync: updatePackageFromMarketPlace } = useUpdatePackageFromMarketPlace()
  223. return useMutation({
  224. mutationFn: (data: {
  225. payload: Dependency[],
  226. plugin: Plugin[],
  227. installedInfo: Record<string, VersionInfo>
  228. }) => {
  229. const { payload, plugin, installedInfo } = data
  230. return Promise.all(payload.map(async (item, i) => {
  231. try {
  232. const orgAndName = `${plugin[i]?.org || plugin[i]?.author}/${plugin[i]?.name}`
  233. const installedPayload = installedInfo[orgAndName]
  234. const isInstalled = !!installedPayload
  235. let uniqueIdentifier = ''
  236. if (item.type === 'github') {
  237. const data = item as GitHubItemAndMarketPlaceDependency
  238. // From local bundle don't have data.value.github_plugin_unique_identifier
  239. uniqueIdentifier = data.value.github_plugin_unique_identifier!
  240. if (!uniqueIdentifier) {
  241. const { unique_identifier } = await post<uploadGitHubResponse>('/workspaces/current/plugin/upload/github', {
  242. body: {
  243. repo: data.value.repo!,
  244. version: data.value.release! || data.value.version!,
  245. package: data.value.packages! || data.value.package!,
  246. },
  247. })
  248. uniqueIdentifier = data.value.github_plugin_unique_identifier! || unique_identifier
  249. // has the same version, but not installed
  250. if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
  251. return {
  252. success: true,
  253. }
  254. }
  255. }
  256. if (!isInstalled) {
  257. await post<InstallPackageResponse>('/workspaces/current/plugin/install/github', {
  258. body: {
  259. repo: data.value.repo!,
  260. version: data.value.release! || data.value.version!,
  261. package: data.value.packages! || data.value.package!,
  262. plugin_unique_identifier: uniqueIdentifier,
  263. },
  264. })
  265. }
  266. }
  267. if (item.type === 'marketplace') {
  268. const data = item as GitHubItemAndMarketPlaceDependency
  269. uniqueIdentifier = data.value.marketplace_plugin_unique_identifier! || plugin[i]?.plugin_id
  270. if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
  271. return {
  272. success: true,
  273. }
  274. }
  275. if (!isInstalled) {
  276. await post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', {
  277. body: {
  278. plugin_unique_identifiers: [uniqueIdentifier],
  279. },
  280. })
  281. }
  282. }
  283. if (item.type === 'package') {
  284. const data = item as PackageDependency
  285. uniqueIdentifier = data.value.unique_identifier
  286. if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
  287. return {
  288. success: true,
  289. }
  290. }
  291. if (!isInstalled) {
  292. await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
  293. body: {
  294. plugin_unique_identifiers: [uniqueIdentifier],
  295. },
  296. })
  297. }
  298. }
  299. if (isInstalled) {
  300. if (item.type === 'package') {
  301. await uninstallPlugin(installedPayload.installedId)
  302. await post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
  303. body: {
  304. plugin_unique_identifiers: [uniqueIdentifier],
  305. },
  306. })
  307. }
  308. else {
  309. await updatePackageFromMarketPlace({
  310. original_plugin_unique_identifier: installedPayload?.uniqueIdentifier,
  311. new_plugin_unique_identifier: uniqueIdentifier,
  312. })
  313. }
  314. }
  315. return ({ success: true })
  316. }
  317. // eslint-disable-next-line unused-imports/no-unused-vars
  318. catch (e) {
  319. return Promise.resolve({ success: false })
  320. }
  321. }))
  322. },
  323. onSuccess,
  324. })
  325. }
  326. export const useDebugKey = () => {
  327. return useQuery({
  328. queryKey: [NAME_SPACE, 'debugKey'],
  329. queryFn: () => get<DebugInfoTypes>('/workspaces/current/plugin/debugging-key'),
  330. })
  331. }
  332. const useReferenceSettingKey = [NAME_SPACE, 'referenceSettings']
  333. export const useReferenceSettings = () => {
  334. return useQuery({
  335. queryKey: useReferenceSettingKey,
  336. queryFn: () => get<ReferenceSetting>('/workspaces/current/plugin/preferences/fetch'),
  337. })
  338. }
  339. export const useInvalidateReferenceSettings = () => {
  340. const queryClient = useQueryClient()
  341. return () => {
  342. queryClient.invalidateQueries(
  343. {
  344. queryKey: useReferenceSettingKey,
  345. })
  346. }
  347. }
  348. export const useMutationReferenceSettings = ({
  349. onSuccess,
  350. }: {
  351. onSuccess?: () => void
  352. }) => {
  353. return useMutation({
  354. mutationFn: (payload: ReferenceSetting) => {
  355. return post('/workspaces/current/plugin/preferences/change', { body: payload })
  356. },
  357. onSuccess,
  358. })
  359. }
  360. export const useRemoveAutoUpgrade = () => {
  361. return useMutation({
  362. mutationFn: (payload: { plugin_id: string }) => {
  363. return post('/workspaces/current/plugin/preferences/autoupgrade/exclude', { body: payload })
  364. },
  365. })
  366. }
  367. export const useMutationPluginsFromMarketplace = () => {
  368. return useMutation({
  369. mutationFn: (pluginsSearchParams: PluginsSearchParams) => {
  370. const {
  371. query,
  372. sortBy,
  373. sortOrder,
  374. category,
  375. tags,
  376. exclude,
  377. type,
  378. page = 1,
  379. pageSize = 40,
  380. } = pluginsSearchParams
  381. const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
  382. return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
  383. body: {
  384. page,
  385. page_size: pageSize,
  386. query,
  387. sort_by: sortBy,
  388. sort_order: sortOrder,
  389. category: category !== 'all' ? category : '',
  390. tags,
  391. exclude,
  392. type,
  393. },
  394. })
  395. },
  396. })
  397. }
  398. export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[], options?: QueryOptions<{ data: PluginsFromMarketplaceResponse }>) => {
  399. return useQuery({
  400. ...options,
  401. queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByIds', unique_identifiers],
  402. queryFn: () => postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/identifier/batch', {
  403. body: {
  404. unique_identifiers,
  405. },
  406. }),
  407. enabled: unique_identifiers?.filter(i => !!i).length > 0,
  408. retry: 0,
  409. })
  410. }
  411. export const useFetchPluginListOrBundleList = (pluginsSearchParams: PluginsSearchParams) => {
  412. return useQuery({
  413. queryKey: [NAME_SPACE, 'fetchPluginListOrBundleList', pluginsSearchParams],
  414. queryFn: () => {
  415. const {
  416. query,
  417. sortBy,
  418. sortOrder,
  419. category,
  420. tags,
  421. exclude,
  422. type,
  423. page = 1,
  424. pageSize = 40,
  425. } = pluginsSearchParams
  426. const pluginOrBundle = type === 'bundle' ? 'bundles' : 'plugins'
  427. return postMarketplace<{ data: PluginsFromMarketplaceResponse }>(`/${pluginOrBundle}/search/advanced`, {
  428. body: {
  429. page,
  430. page_size: pageSize,
  431. query,
  432. sort_by: sortBy,
  433. sort_order: sortOrder,
  434. category: category !== 'all' ? category : '',
  435. tags,
  436. exclude,
  437. type,
  438. },
  439. })
  440. },
  441. })
  442. }
  443. export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[]) => {
  444. return useQuery({
  445. queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByInfo', infos],
  446. queryFn: () => postMarketplace<{ data: PluginsFromMarketplaceByInfoResponse }>('/plugins/versions/batch', {
  447. body: {
  448. plugin_tuples: infos.map(info => ({
  449. org: info.organization,
  450. name: info.plugin,
  451. version: info.version,
  452. })),
  453. },
  454. }),
  455. enabled: infos?.filter(i => !!i).length > 0,
  456. retry: 0,
  457. })
  458. }
  459. const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
  460. export const usePluginTaskList = (category?: PluginType) => {
  461. const {
  462. canManagement,
  463. } = useReferenceSetting()
  464. const { refreshPluginList } = useRefreshPluginList()
  465. const {
  466. data,
  467. isFetched,
  468. isRefetching,
  469. refetch,
  470. ...rest
  471. } = useQuery({
  472. enabled: canManagement,
  473. queryKey: usePluginTaskListKey,
  474. queryFn: () => get<{ tasks: PluginTask[] }>('/workspaces/current/plugin/tasks?page=1&page_size=100'),
  475. refetchInterval: (lastQuery) => {
  476. const lastData = lastQuery.state.data
  477. const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
  478. return taskDone ? false : 5000
  479. },
  480. })
  481. useEffect(() => {
  482. // After first fetch, refresh plugin list each time all tasks are done
  483. if (!isRefetching) {
  484. const lastData = cloneDeep(data)
  485. const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
  486. const taskAllFailed = lastData?.tasks.every(task => task.status === TaskStatus.failed)
  487. if (taskDone) {
  488. if (lastData?.tasks.length && !taskAllFailed)
  489. refreshPluginList(category ? { category } as any : undefined, !category)
  490. }
  491. }
  492. }, [isRefetching])
  493. const handleRefetch = useCallback(() => {
  494. refetch()
  495. }, [refetch])
  496. return {
  497. data,
  498. pluginTasks: data?.tasks || [],
  499. isFetched,
  500. handleRefetch,
  501. ...rest,
  502. }
  503. }
  504. export const useMutationClearTaskPlugin = () => {
  505. return useMutation({
  506. mutationFn: ({ taskId, pluginId }: { taskId: string; pluginId: string }) => {
  507. return post<{ success: boolean }>(`/workspaces/current/plugin/tasks/${taskId}/delete/${pluginId}`)
  508. },
  509. })
  510. }
  511. export const useMutationClearAllTaskPlugin = () => {
  512. return useMutation({
  513. mutationFn: () => {
  514. return post<{ success: boolean }>('/workspaces/current/plugin/tasks/delete_all')
  515. },
  516. })
  517. }
  518. export const usePluginManifestInfo = (pluginUID: string) => {
  519. return useQuery({
  520. enabled: !!pluginUID,
  521. queryKey: [[NAME_SPACE, 'manifest', pluginUID]],
  522. queryFn: () => getMarketplace<{ data: { plugin: PluginInfoFromMarketPlace, version: { version: string } } }>(`/plugins/${pluginUID}`),
  523. retry: 0,
  524. })
  525. }
  526. export const useDownloadPlugin = (info: { organization: string; pluginName: string; version: string }, needDownload: boolean) => {
  527. return useQuery({
  528. queryKey: [NAME_SPACE, 'downloadPlugin', info],
  529. queryFn: () => getMarketplace<Blob>(`/plugins/${info.organization}/${info.pluginName}/${info.version}/download`),
  530. enabled: needDownload,
  531. retry: 0,
  532. })
  533. }
  534. export const useMutationCheckDependencies = () => {
  535. return useMutation({
  536. mutationFn: (appId: string) => {
  537. return get<{ leaked_dependencies: Dependency[] }>(`/apps/imports/${appId}/check-dependencies`)
  538. },
  539. })
  540. }
  541. export const useModelInList = (currentProvider?: ModelProvider, modelId?: string) => {
  542. return useQuery({
  543. queryKey: ['modelInList', currentProvider?.provider, modelId],
  544. queryFn: async () => {
  545. if (!modelId || !currentProvider) return false
  546. try {
  547. const modelsData = await fetchModelProviderModelList(`/workspaces/current/model-providers/${currentProvider?.provider}/models`)
  548. return !!modelId && !!modelsData.data.find(item => item.model === modelId)
  549. }
  550. catch {
  551. return false
  552. }
  553. },
  554. enabled: !!modelId && !!currentProvider,
  555. })
  556. }
  557. export const usePluginInfo = (providerName?: string) => {
  558. return useQuery({
  559. queryKey: ['pluginInfo', providerName],
  560. queryFn: async () => {
  561. if (!providerName) return null
  562. const parts = providerName.split('/')
  563. const org = parts[0]
  564. const name = parts[1]
  565. try {
  566. const response = await fetchPluginInfoFromMarketPlace({ org, name })
  567. return response.data.plugin.category === PluginTypeEnum.model ? response.data.plugin : null
  568. }
  569. catch {
  570. return null
  571. }
  572. },
  573. enabled: !!providerName,
  574. })
  575. }
  576. export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type: 'tool') => {
  577. return useMutation({
  578. mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', {
  579. params: {
  580. plugin_id,
  581. provider,
  582. action,
  583. parameter,
  584. provider_type,
  585. },
  586. }),
  587. })
  588. }