Files
dify/web/service/use-plugins.ts
Stephen Zhou eb71e47f3b chore: migrate linting to vp lint with ESLint fallback (#38834)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-13 06:41:53 +00:00

1491 lines
48 KiB
TypeScript

import type { PluginInstallationItemResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type {
PluginInfoFromMarketPlace,
PluginsFromMarketplaceByInfoResponse,
PluginsFromMarketplaceResponse,
} from '@dify/contracts/marketplace'
import type { MutateOptions, QueryClient, QueryOptions } from '@tanstack/react-query'
import type {
FormOption,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { AutoUpdateConfig } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
import type {
DebugInfo as DebugInfoTypes,
Dependency,
GitHubItemAndMarketPlaceDependency,
InstalledPluginCategoryListResponse,
InstalledPluginListWithTotalResponse,
InstallPackageResponse,
InstallStatusResponse,
MetaData,
PackageDependency,
Permissions,
Plugin,
PluginDeclaration,
PluginDetail,
PluginTask,
PluginTaskStart,
ReferenceSetting,
uploadGitHubResponse,
VersionInfo,
VersionListResponse,
} from '@/app/components/plugins/types'
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { cloneDeep } from 'es-toolkit/object'
import { useAtomValue } from 'jotai'
import { useCallback, useEffect, useRef } from 'react'
import { FormTypeEnum } from '@/app/components/base/form/types'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
import { PluginCategoryEnum, PluginSource, TaskStatus } from '@/app/components/plugins/types'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { fetchModelProviderModelList } from '@/service/common'
import { fetchPluginInfoFromMarketPlace, uninstallPlugin } from '@/service/plugins'
import { hasPermission } from '@/utils/permission'
// oxlint-disable-next-line no-restricted-imports
import { get, getMarketplace, post, postMarketplace } from './base'
import { consoleQuery } from './client'
import { useInvalidateAllBuiltInTools } from './use-tools'
const NAME_SPACE = 'plugins'
const useInstalledPluginListKey = [NAME_SPACE, 'installedPluginList']
const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
type PluginTaskListResponse = {
tasks: PluginTask[]
}
const getString = (value: unknown) => {
return typeof value === 'string' ? value : ''
}
const getBoolean = (value: unknown) => {
return value === true
}
const getNumber = (value: unknown) => {
return typeof value === 'number' ? value : undefined
}
const isRecord = (value: unknown): value is Record<string, unknown> => {
return !!value && typeof value === 'object' && !Array.isArray(value)
}
const getRecord = (value: unknown, key: string) => {
if (!isRecord(value)) return undefined
const child = value[key]
return isRecord(child) ? child : undefined
}
const getRecordArray = (value: unknown, key: string) => {
if (!isRecord(value)) return []
const child = value[key]
return Array.isArray(child) ? child.filter(isRecord) : []
}
const getStringArray = (value: unknown) => {
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : []
}
const getI18nValue = (value: object | null | undefined, key: string) => {
return value ? getString(Object.entries(value).find(([itemKey]) => itemKey === key)?.[1]) : ''
}
const normalizeI18nObject = (
value: object | null | undefined,
fallback = '',
): PluginDeclaration['label'] => {
const en = getI18nValue(value, 'en_US') || getI18nValue(value, 'en-US') || fallback
const zhHans = getI18nValue(value, 'zh_Hans') || getI18nValue(value, 'zh-Hans') || en
const ja = getI18nValue(value, 'ja_JP') || getI18nValue(value, 'ja-JP') || en
const ptBr = getI18nValue(value, 'pt_BR') || getI18nValue(value, 'pt-BR') || en
return {
'en-US': en,
'zh-Hans': zhHans,
'zh-Hant': en,
'pt-BR': ptBr,
'es-ES': en,
'fr-FR': en,
'de-DE': en,
'ja-JP': ja,
'ko-KR': en,
'ru-RU': en,
'it-IT': en,
'th-TH': en,
'uk-UA': en,
'vi-VN': en,
'ro-RO': en,
'pl-PL': en,
'hi-IN': en,
'tr-TR': en,
'fa-IR': en,
'sl-SI': en,
'id-ID': en,
'nl-NL': en,
'ar-TN': en,
en_US: en,
zh_Hans: zhHans,
ja_JP: ja,
}
}
const normalizePluginCategory = (
category: PluginInstallationItemResponse['declaration']['category'],
): PluginCategoryEnum => {
switch (category) {
case PluginCategoryEnum.tool:
return PluginCategoryEnum.tool
case PluginCategoryEnum.model:
return PluginCategoryEnum.model
case PluginCategoryEnum.datasource:
return PluginCategoryEnum.datasource
case PluginCategoryEnum.trigger:
return PluginCategoryEnum.trigger
case PluginCategoryEnum.agent:
return PluginCategoryEnum.agent
case PluginCategoryEnum.extension:
return PluginCategoryEnum.extension
}
return PluginCategoryEnum.extension
}
const normalizePluginSource = (source: PluginInstallationItemResponse['source']): PluginSource => {
switch (source) {
case PluginSource.github:
return PluginSource.github
case PluginSource.marketplace:
return PluginSource.marketplace
case PluginSource.local:
return PluginSource.local
case PluginSource.debugging:
return PluginSource.debugging
}
return PluginSource.marketplace
}
const normalizePluginMeta = (meta: Record<string, unknown>): MetaData => {
return {
repo: getString(meta.repo),
version: getString(meta.version),
package: getString(meta.package),
}
}
const normalizeToolCredentialOption = (
option: Record<string, unknown>,
): NonNullable<
NonNullable<PluginDeclaration['tool']>['credentials_schema'][number]['options']
>[number] => ({
label: normalizeI18nObject(getRecord(option, 'label'), getString(option.value)),
value: getString(option.value),
})
const normalizeToolCredential = (
credential: Record<string, unknown>,
): NonNullable<PluginDeclaration['tool']>['credentials_schema'][number] => ({
name: getString(credential.name),
label: normalizeI18nObject(getRecord(credential, 'label'), getString(credential.name)),
help: credential.help === null ? null : normalizeI18nObject(getRecord(credential, 'help')),
placeholder: normalizeI18nObject(getRecord(credential, 'placeholder')),
type: getString(credential.type),
required: getBoolean(credential.required),
default: getString(credential.default),
options: getRecordArray(credential, 'options').map(normalizeToolCredentialOption),
})
const normalizePluginToolDeclaration = (value: unknown): PluginDeclaration['tool'] => {
if (!isRecord(value)) return undefined
const identity = getRecord(value, 'identity')
if (!identity) return undefined
const name = getString(identity.name)
return {
identity: {
author: getString(identity.author),
name,
description: normalizeI18nObject(getRecord(identity, 'description')),
icon: getString(identity.icon),
label: normalizeI18nObject(getRecord(identity, 'label'), name),
tags: getStringArray(identity.tags),
},
credentials_schema: getRecordArray(value, 'credentials_schema').map(normalizeToolCredential),
}
}
const normalizePluginEndpointDeclaration = (value: unknown): PluginDeclaration['endpoint'] => {
if (!isRecord(value)) return undefined
return {
settings: getRecordArray(value, 'settings').map(normalizeToolCredential),
endpoints: getRecordArray(value, 'endpoints').map((endpoint) => ({
path: getString(endpoint.path),
method: getString(endpoint.method),
hidden: endpoint.hidden === undefined ? undefined : getBoolean(endpoint.hidden),
})),
}
}
const normalizeParameterDefault = (value: unknown) => {
if (Array.isArray(value)) return value.filter((item) => typeof item === 'string')
if (typeof value === 'string') return value
if (value === undefined || value === null) return undefined
return String(value)
}
const normalizeFormType = (type: unknown): FormTypeEnum => {
switch (type) {
case FormTypeEnum.appSelector:
return FormTypeEnum.appSelector
case FormTypeEnum.boolean:
return FormTypeEnum.boolean
case FormTypeEnum.checkbox:
return FormTypeEnum.checkbox
case FormTypeEnum.dynamicSelect:
return FormTypeEnum.dynamicSelect
case FormTypeEnum.file:
return FormTypeEnum.file
case FormTypeEnum.files:
return FormTypeEnum.files
case FormTypeEnum.modelSelector:
return FormTypeEnum.modelSelector
case FormTypeEnum.multiToolSelector:
return FormTypeEnum.multiToolSelector
case FormTypeEnum.radio:
return FormTypeEnum.radio
case FormTypeEnum.secretInput:
return FormTypeEnum.secretInput
case FormTypeEnum.select:
return FormTypeEnum.select
case FormTypeEnum.textNumber:
case 'number':
return FormTypeEnum.textNumber
case FormTypeEnum.textInput:
case 'string':
case 'array':
case 'object':
return FormTypeEnum.textInput
}
return FormTypeEnum.textInput
}
const normalizeParameterOption = (
option: Record<string, unknown>,
): NonNullable<PluginDeclaration['trigger']['subscription_schema'][number]['options']>[number] => ({
value: getString(option.value),
label: normalizeI18nObject(getRecord(option, 'label'), getString(option.value)),
icon: getString(option.icon) || undefined,
})
const normalizeParameterSchema = (
parameter: Record<string, unknown>,
): PluginDeclaration['trigger']['subscription_schema'][number] => ({
name: getString(parameter.name),
label: normalizeI18nObject(getRecord(parameter, 'label'), getString(parameter.name)),
type: normalizeFormType(parameter.type),
auto_generate: parameter.auto_generate ?? undefined,
template: parameter.template ?? undefined,
scope: parameter.scope ?? undefined,
required: getBoolean(parameter.required),
multiple: getBoolean(parameter.multiple),
default: normalizeParameterDefault(parameter.default),
min: parameter.min ?? undefined,
max: parameter.max ?? undefined,
precision: parameter.precision ?? undefined,
options: getRecordArray(parameter, 'options').map(normalizeParameterOption),
description: normalizeI18nObject(getRecord(parameter, 'description')),
})
const normalizeCredentialSchema = (
schema: Record<string, unknown>,
): PluginDeclaration['trigger']['subscription_constructor']['credentials_schema'][number] => ({
name: getString(schema.name),
label: normalizeI18nObject(getRecord(schema, 'label'), getString(schema.name)),
description: normalizeI18nObject(getRecord(schema, 'description')),
type: normalizeFormType(schema.type),
scope: schema.scope ?? undefined,
required: getBoolean(schema.required),
default: schema.default ?? undefined,
options: getRecordArray(schema, 'options').map(normalizeParameterOption),
help: normalizeI18nObject(getRecord(schema, 'help')),
url: getString(schema.url),
placeholder: normalizeI18nObject(getRecord(schema, 'placeholder')),
})
const normalizeTriggerEventParameter = (
parameter: Record<string, unknown>,
): PluginDeclaration['trigger']['events'][number]['parameters'][number] => ({
name: getString(parameter.name),
label: normalizeI18nObject(getRecord(parameter, 'label'), getString(parameter.name)),
type: getString(parameter.type),
auto_generate: parameter.auto_generate ?? undefined,
template: parameter.template ?? undefined,
scope: parameter.scope ?? undefined,
required: getBoolean(parameter.required),
multiple: getBoolean(parameter.multiple),
default: parameter.default ?? '',
min: getNumber(parameter.min),
max: getNumber(parameter.max),
precision: getNumber(parameter.precision),
options: getRecordArray(parameter, 'options').map(normalizeParameterOption),
description: normalizeI18nObject(getRecord(parameter, 'description')),
})
const normalizeTriggerEvent = (
event: Record<string, unknown>,
): PluginDeclaration['trigger']['events'][number] => {
const identity = getRecord(event, 'identity')
const name = getString(event.name)
return {
name,
identity: {
author: getString(identity?.author),
name: getString(identity?.name) || name,
label: normalizeI18nObject(getRecord(identity, 'label'), name),
provider: getString(identity?.provider) || undefined,
},
description: normalizeI18nObject(getRecord(event, 'description'), name),
parameters: getRecordArray(event, 'parameters').map(normalizeTriggerEventParameter),
output_schema: getRecord(event, 'output_schema') ?? {},
}
}
const createEmptyTrigger = (name: string): PluginDeclaration['trigger'] => ({
events: [],
identity: {
author: '',
name,
label: normalizeI18nObject(undefined, name),
description: normalizeI18nObject(undefined),
icon: '',
tags: [],
},
subscription_constructor: {
credentials_schema: [],
oauth_schema: {
client_schema: [],
credentials_schema: [],
},
parameters: [],
},
subscription_schema: [],
})
const normalizePluginTriggerDeclaration = (
value: unknown,
fallbackName: string,
): PluginDeclaration['trigger'] => {
if (!isRecord(value)) return createEmptyTrigger(fallbackName)
const identity = getRecord(value, 'identity')
const subscriptionConstructor = getRecord(value, 'subscription_constructor')
const oauthSchema = getRecord(subscriptionConstructor, 'oauth_schema')
return {
events: getRecordArray(value, 'events').map(normalizeTriggerEvent),
identity: {
author: getString(identity?.author),
name: getString(identity?.name) || fallbackName,
label: normalizeI18nObject(getRecord(identity, 'label'), fallbackName),
description: normalizeI18nObject(getRecord(identity, 'description')),
icon: getString(identity?.icon),
tags: getStringArray(identity?.tags),
},
subscription_constructor: {
credentials_schema: getRecordArray(subscriptionConstructor, 'credentials_schema').map(
normalizeCredentialSchema,
),
oauth_schema: {
client_schema: getRecordArray(oauthSchema, 'client_schema').map(normalizeCredentialSchema),
credentials_schema: getRecordArray(oauthSchema, 'credentials_schema').map(
normalizeCredentialSchema,
),
},
parameters: getRecordArray(subscriptionConstructor, 'parameters').map(
normalizeParameterSchema,
),
},
subscription_schema: getRecordArray(value, 'subscription_schema').map(normalizeParameterSchema),
}
}
const normalizePluginDeclaration = (plugin: PluginInstallationItemResponse): PluginDeclaration => {
const { declaration } = plugin
return {
plugin_unique_identifier: plugin.plugin_unique_identifier,
version: declaration.version,
author: declaration.author ?? '',
icon: declaration.icon,
icon_dark: declaration.icon_dark ?? undefined,
name: declaration.name,
category: normalizePluginCategory(declaration.category),
label: normalizeI18nObject(declaration.label, declaration.name),
description: normalizeI18nObject(declaration.description, declaration.name),
created_at: declaration.created_at,
resource: declaration.resource,
plugins: declaration.plugins,
verified: declaration.verified ?? false,
endpoint: normalizePluginEndpointDeclaration(declaration.endpoint),
tool: normalizePluginToolDeclaration(declaration.tool),
datasource: normalizePluginToolDeclaration(declaration.datasource),
model: declaration.model,
tags: declaration.tags ?? [],
agent_strategy: declaration.agent_strategy,
meta: {
version: getString(declaration.meta.version) || declaration.version,
minimum_dify_version: getString(declaration.meta.minimum_dify_version) || undefined,
},
trigger: normalizePluginTriggerDeclaration(declaration.trigger, declaration.name),
}
}
export const normalizeInstalledPluginDetail = (
plugin: PluginInstallationItemResponse,
): PluginDetail => {
const declaration = normalizePluginDeclaration(plugin)
return {
id: plugin.id,
created_at: plugin.created_at,
updated_at: plugin.updated_at,
name: declaration.name,
plugin_id: plugin.plugin_id,
plugin_unique_identifier: plugin.plugin_unique_identifier,
declaration,
installation_id: plugin.id,
tenant_id: plugin.tenant_id,
endpoints_setups: plugin.endpoints_setups,
endpoints_active: plugin.endpoints_active,
version: plugin.version,
latest_version: plugin.version,
latest_unique_identifier: plugin.plugin_unique_identifier,
source: normalizePluginSource(plugin.source),
meta: normalizePluginMeta(plugin.meta),
status: 'active',
deprecated_reason: '',
alternative_plugin_id: '',
}
}
const isUnfinishedPluginTask = (task: PluginTask) =>
task.status === TaskStatus.pending || task.status === TaskStatus.running
const normalizeStartedPluginTask = (task: PluginTaskStart): PluginTask => ({
...task,
plugins: task.plugins.map((plugin) => ({
...plugin,
taskId: plugin.taskId || task.id,
})),
})
const upsertStartedPluginTask = (queryClient: QueryClient, response: InstallPackageResponse) => {
if (!response.task) return
const startedTask = normalizeStartedPluginTask(response.task)
queryClient.setQueryData<PluginTaskListResponse>(usePluginTaskListKey, (previous) => {
if (!previous) return { tasks: [startedTask] }
const existingTaskIndex = previous.tasks.findIndex((task) => task.id === startedTask.id)
if (existingTaskIndex === -1) return { tasks: [startedTask, ...previous.tasks] }
return {
tasks: previous.tasks.map((task) => (task.id === startedTask.id ? startedTask : task)),
}
})
}
const preserveLocalUnfinishedPluginTasks = (
previousData: PluginTaskListResponse | undefined,
nextData: PluginTaskListResponse,
) => {
if (!previousData) return nextData
const nextTaskIds = new Set(nextData.tasks.map((task) => task.id))
const missingUnfinishedTasks = previousData.tasks.filter(
(task) => !nextTaskIds.has(task.id) && isUnfinishedPluginTask(task),
)
if (!missingUnfinishedTasks.length) return nextData
return {
...nextData,
tasks: [...missingUnfinishedTasks, ...nextData.tasks],
}
}
export const useCheckInstalled = ({
pluginIds,
enabled,
}: {
pluginIds: string[]
enabled: boolean
}) => {
return useQuery(
consoleQuery.workspaces.current.plugin.list.installations.ids.post.queryOptions({
input: { body: { plugin_ids: pluginIds } },
enabled,
staleTime: 0,
select: (response) => ({
plugins: response.plugins.map(normalizeInstalledPluginDetail),
}),
}),
)
}
export const useInvalidateCheckInstalled = () => {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({
queryKey: consoleQuery.workspaces.current.plugin.list.installations.ids.post.key(),
})
}
}
const useRecommendedMarketplacePluginsKey = [NAME_SPACE, 'recommendedMarketplacePlugins']
const useRecommendedMarketplacePlugins = ({
collection = '__recommended-plugins-tools',
enabled = true,
limit = 15,
}: {
collection?: string
enabled?: boolean
limit?: number
} = {}) => {
return useQuery<Plugin[]>({
queryKey: [...useRecommendedMarketplacePluginsKey, collection, limit],
queryFn: async () => {
const response = await postMarketplace<{ data: { plugins: Plugin[] } }>(
`/collections/${collection}/plugins`,
{
body: {
limit,
},
},
)
return response.data.plugins.map((plugin) => getFormattedPlugin(plugin))
},
enabled,
staleTime: 60 * 1000,
})
}
export const useFeaturedToolsRecommendations = (enabled: boolean, limit = 15) => {
const { data: plugins = [], isLoading } = useRecommendedMarketplacePlugins({
collection: '__recommended-plugins-tools',
enabled,
limit,
})
return {
plugins,
isLoading,
}
}
export const useFeaturedTriggersRecommendations = (enabled: boolean, limit = 15) => {
const { data: plugins = [], isLoading } = useRecommendedMarketplacePlugins({
collection: '__recommended-plugins-triggers',
enabled,
limit,
})
return {
plugins,
isLoading,
}
}
type UseInstalledPluginListOptions = {
category?: PluginCategoryEnum
refetchOnMount?: boolean | 'always'
}
export const useInstalledPluginList = (
disable?: boolean,
pageSize = 100,
options?: UseInstalledPluginListOptions,
) => {
const category = options?.category
const fetchPlugins = async ({ pageParam = 1 }) => {
const path = category
? `/workspaces/current/plugin/${category}/list`
: '/workspaces/current/plugin/list'
if (category)
return get<InstalledPluginCategoryListResponse>(
`${path}?page=${pageParam}&page_size=${pageSize}`,
)
return get<InstalledPluginListWithTotalResponse>(
`${path}?page=${pageParam}&page_size=${pageSize}`,
)
}
const { data, error, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isSuccess } =
useInfiniteQuery({
enabled: !disable,
queryKey: category ? [...useInstalledPluginListKey, category] : useInstalledPluginListKey,
queryFn: fetchPlugins,
getNextPageParam: (lastPage, pages) => {
if (category)
return 'has_more' in lastPage && lastPage.has_more ? pages.length + 1 : undefined
const totalItems = 'total' in lastPage ? lastPage.total : 0
const currentPage = pages.length
const itemsLoaded = currentPage * pageSize
if (itemsLoaded >= totalItems) return
return currentPage + 1
},
initialPageParam: 1,
refetchOnMount: options?.refetchOnMount,
})
const plugins = data?.pages.flatMap((page) => page.plugins) ?? []
const firstPage = data?.pages[0]
const builtinTools = firstPage && 'builtin_tools' in firstPage ? firstPage.builtin_tools : []
const total = data?.pages[0] && 'total' in data.pages[0] ? data.pages[0].total : plugins.length
return {
data: disable
? undefined
: {
plugins,
builtin_tools: builtinTools,
total,
},
isLastPage: !hasNextPage,
loadNextPage: () => {
fetchNextPage()
},
isLoading,
isFetching: isFetchingNextPage,
error,
isSuccess,
}
}
export const useInvalidateInstalledPluginList = () => {
const queryClient = useQueryClient()
const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
return () => {
queryClient.invalidateQueries({
queryKey: useInstalledPluginListKey,
})
invalidateAllBuiltInTools()
}
}
export const useInstallPackageFromMarketPlace = (
options?: MutateOptions<InstallPackageResponse, Error, string>,
) => {
const queryClient = useQueryClient()
return useMutation({
...options,
mutationFn: (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', {
body: { plugin_unique_identifiers: [uniqueIdentifier] },
})
},
onSuccess: (data, variables, context, mutation) => {
upsertStartedPluginTask(queryClient, data)
options?.onSuccess?.(data, variables, context, mutation)
},
})
}
export const useUpdatePackageFromMarketPlace = (
options?: MutateOptions<InstallPackageResponse, Error, object>,
) => {
const queryClient = useQueryClient()
return useMutation({
...options,
mutationFn: (body: object) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/upgrade/marketplace', {
body,
})
},
onSuccess: (data, variables, context, mutation) => {
upsertStartedPluginTask(queryClient, data)
options?.onSuccess?.(data, variables, context, mutation)
},
})
}
export const usePluginDeclarationFromMarketPlace = (pluginUniqueIdentifier: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'pluginDeclaration', pluginUniqueIdentifier],
queryFn: () =>
get<{ manifest: PluginDeclaration }>('/workspaces/current/plugin/marketplace/pkg', {
params: { plugin_unique_identifier: pluginUniqueIdentifier },
}),
enabled: !!pluginUniqueIdentifier,
})
}
export const useVersionListOfPlugin = (pluginID: string) => {
return useQuery<{ data: VersionListResponse }>({
enabled: !!pluginID,
queryKey: [NAME_SPACE, 'versions', pluginID],
queryFn: () =>
getMarketplace<{ data: VersionListResponse }>(`/plugins/${pluginID}/versions`, {
params: { page: 1, page_size: 100 },
}),
})
}
export const useInstallPackageFromLocal = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/pkg', {
body: { plugin_unique_identifiers: [uniqueIdentifier] },
})
},
onSuccess: (data) => {
upsertStartedPluginTask(queryClient, data)
},
})
}
export const useInstallPackageFromGitHub = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({
repoUrl,
selectedVersion,
selectedPackage,
uniqueIdentifier,
}: {
repoUrl: string
selectedVersion: string
selectedPackage: string
uniqueIdentifier: string
}) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/github', {
body: {
repo: repoUrl,
version: selectedVersion,
package: selectedPackage,
plugin_unique_identifier: uniqueIdentifier,
},
})
},
onSuccess: (data) => {
upsertStartedPluginTask(queryClient, data)
},
})
}
export const useUploadGitHub = (payload: { repo: string; version: string; package: string }) => {
return useQuery({
queryKey: [NAME_SPACE, 'uploadGitHub', payload],
queryFn: () =>
post<uploadGitHubResponse>('/workspaces/current/plugin/upload/github', {
body: payload,
}),
retry: 0,
})
}
export const useInstallOrUpdate = ({
onSuccess,
}: {
onSuccess?: (res: InstallStatusResponse[]) => void
}) => {
const queryClient = useQueryClient()
const { mutateAsync: updatePackageFromMarketPlace } = useUpdatePackageFromMarketPlace()
return useMutation({
mutationFn: (data: {
payload: Dependency[]
plugin: Plugin[]
installedInfo: Record<string, VersionInfo>
}) => {
const { payload, plugin, installedInfo } = data
return Promise.all(
payload.map(async (item, i) => {
try {
const orgAndName = `${plugin[i]?.org || plugin[i]?.author}/${plugin[i]?.name}`
const installedPayload = installedInfo[orgAndName]
const isInstalled = !!installedPayload
let uniqueIdentifier = ''
let taskId = ''
let isFinishedInstallation = false
if (item.type === 'github') {
const data = item as GitHubItemAndMarketPlaceDependency
// From local bundle don't have data.value.github_plugin_unique_identifier
uniqueIdentifier = data.value.github_plugin_unique_identifier!
if (!uniqueIdentifier) {
const { unique_identifier } = await post<uploadGitHubResponse>(
'/workspaces/current/plugin/upload/github',
{
body: {
repo: data.value.repo!,
version: data.value.release! || data.value.version!,
package: data.value.packages! || data.value.package!,
},
},
)
uniqueIdentifier = data.value.github_plugin_unique_identifier! || unique_identifier
// has the same version, but not installed
if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
return {
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
}
if (!isInstalled) {
const response = await post<InstallPackageResponse>(
'/workspaces/current/plugin/install/github',
{
body: {
repo: data.value.repo!,
version: data.value.release! || data.value.version!,
package: data.value.packages! || data.value.package!,
plugin_unique_identifier: uniqueIdentifier,
},
},
)
upsertStartedPluginTask(queryClient, response)
const { task_id, all_installed } = response
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (item.type === 'marketplace') {
const data = item as GitHubItemAndMarketPlaceDependency
uniqueIdentifier =
data.value.marketplace_plugin_unique_identifier! ||
(plugin[i]?.latest_package_identifier ?? '') ||
(plugin[i]?.plugin_id ?? '')
if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
return {
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
if (!isInstalled) {
const response = await post<InstallPackageResponse>(
'/workspaces/current/plugin/install/marketplace',
{
body: {
plugin_unique_identifiers: [uniqueIdentifier],
},
},
)
upsertStartedPluginTask(queryClient, response)
const { task_id, all_installed } = response
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (item.type === 'package') {
const data = item as PackageDependency
uniqueIdentifier = data.value.unique_identifier
if (uniqueIdentifier === installedPayload?.uniqueIdentifier) {
return {
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
}
if (!isInstalled) {
const response = await post<InstallPackageResponse>(
'/workspaces/current/plugin/install/pkg',
{
body: {
plugin_unique_identifiers: [uniqueIdentifier],
},
},
)
upsertStartedPluginTask(queryClient, response)
const { task_id, all_installed } = response
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (isInstalled) {
if (item.type === 'package') {
await uninstallPlugin(installedPayload.installedId)
const response = await post<InstallPackageResponse>(
'/workspaces/current/plugin/install/pkg',
{
body: {
plugin_unique_identifiers: [uniqueIdentifier],
},
},
)
upsertStartedPluginTask(queryClient, response)
const { task_id, all_installed } = response
taskId = task_id
isFinishedInstallation = all_installed
} else {
const response = await updatePackageFromMarketPlace({
original_plugin_unique_identifier: installedPayload?.uniqueIdentifier,
new_plugin_unique_identifier: uniqueIdentifier,
})
const { task_id, all_installed } = response
taskId = task_id
isFinishedInstallation = all_installed
}
}
if (isFinishedInstallation) {
return {
status: TaskStatus.success,
taskId: '',
uniqueIdentifier: '',
}
} else {
return {
status: TaskStatus.running,
taskId,
uniqueIdentifier,
}
}
} catch {
return Promise.resolve({ status: TaskStatus.failed, taskId: '', uniqueIdentifier: '' })
}
}),
)
},
onSuccess,
})
}
export const useDebugKey = () => {
return useQuery({
queryKey: [NAME_SPACE, 'debugKey'],
queryFn: () => get<DebugInfoTypes>('/workspaces/current/plugin/debugging-key'),
})
}
type PluginAutoUpgradeSettingsResponse = {
category: PluginCategoryEnum
auto_upgrade: AutoUpdateConfig
}
type MarketplacePluginInfoRequest = {
organization?: string
plugin?: string
version?: string
}
const useReferenceSettingKey = [NAME_SPACE, 'referenceSettings']
const usePluginPermissionSettingsKey = [...useReferenceSettingKey, 'permission']
const usePluginAutoUpgradeSettingsKey = [...useReferenceSettingKey, 'autoUpgrade']
const pluginAutoUpgradeSettingsQueryKey = (category: PluginCategoryEnum) => [
...usePluginAutoUpgradeSettingsKey,
category,
]
const areStringArraysEqual = (left: string[], right: string[]) => {
return left.length === right.length && left.every((value, index) => value === right[index])
}
const arePermissionsEqual = (left: Permissions | undefined, right: Permissions | undefined) => {
return (
left?.install_permission === right?.install_permission &&
left?.debug_permission === right?.debug_permission
)
}
const areAutoUpgradeSettingsEqual = (
left: AutoUpdateConfig | undefined,
right: AutoUpdateConfig | undefined,
) => {
return (
left?.strategy_setting === right?.strategy_setting &&
left?.upgrade_time_of_day === right?.upgrade_time_of_day &&
left?.upgrade_mode === right?.upgrade_mode &&
areStringArraysEqual(left?.exclude_plugins ?? [], right?.exclude_plugins ?? []) &&
areStringArraysEqual(left?.include_plugins ?? [], right?.include_plugins ?? [])
)
}
// legacy plugin permission
// export const hasPluginPermission = (permission: PermissionType | undefined, isAdmin: boolean) => {
// if (!permission)
// return false
// if (permission === PermissionType.noOne)
// return false
// if (permission === PermissionType.everyone)
// return true
// return isAdmin
// }
export const usePluginPermissionSettings = () => {
return useQuery({
queryKey: usePluginPermissionSettingsKey,
queryFn: () => get<Permissions>('/workspaces/current/plugin/permission/fetch'),
})
}
export const usePluginAutoUpgradeSettings = (category: PluginCategoryEnum) => {
return useQuery({
queryKey: pluginAutoUpgradeSettingsQueryKey(category),
queryFn: () =>
get<PluginAutoUpgradeSettingsResponse>('/workspaces/current/plugin/auto-upgrade/fetch', {
params: { category },
}),
staleTime: 60 * 1000,
})
}
export const useInvalidateReferenceSettings = () => {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({
queryKey: useReferenceSettingKey,
})
}
}
export const useMutationPluginPermissionSettings = ({
onSuccess,
}: {
onSuccess?: () => void
} = {}) => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (payload: Permissions) => {
return post('/workspaces/current/plugin/permission/change', { body: payload })
},
onMutate: async (payload) => {
await queryClient.cancelQueries({ queryKey: usePluginPermissionSettingsKey })
const previousPermission = queryClient.getQueryData<Permissions>(
usePluginPermissionSettingsKey,
)
const hadPreviousPermission = previousPermission !== undefined
queryClient.setQueryData(usePluginPermissionSettingsKey, payload)
return { previousPermission, hadPreviousPermission }
},
onError: (_error, _payload, context) => {
if (context?.hadPreviousPermission)
queryClient.setQueryData(usePluginPermissionSettingsKey, context.previousPermission)
else queryClient.removeQueries({ queryKey: usePluginPermissionSettingsKey })
},
onSuccess: () => {
onSuccess?.()
},
})
}
export const useMutationPluginAutoUpgradeSettings = ({
category,
onSuccess,
}: {
category: PluginCategoryEnum
onSuccess?: () => void
}) => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (payload: AutoUpdateConfig) => {
return post('/workspaces/current/plugin/auto-upgrade/change', {
body: {
category,
auto_upgrade: payload,
},
})
},
onMutate: async (payload) => {
const queryKey = pluginAutoUpgradeSettingsQueryKey(category)
await queryClient.cancelQueries({ queryKey })
const previousAutoUpgrade =
queryClient.getQueryData<PluginAutoUpgradeSettingsResponse>(queryKey)
const hadPreviousAutoUpgrade = previousAutoUpgrade !== undefined
queryClient.setQueryData(pluginAutoUpgradeSettingsQueryKey(category), {
category,
auto_upgrade: payload,
} satisfies PluginAutoUpgradeSettingsResponse)
return { previousAutoUpgrade, hadPreviousAutoUpgrade }
},
onError: (_error, _payload, context) => {
if (context?.hadPreviousAutoUpgrade)
queryClient.setQueryData(
pluginAutoUpgradeSettingsQueryKey(category),
context.previousAutoUpgrade,
)
else queryClient.removeQueries({ queryKey: pluginAutoUpgradeSettingsQueryKey(category) })
},
onSuccess: () => {
onSuccess?.()
},
})
}
export const useMutationReferenceSettings = ({
category,
currentReferenceSetting,
onSuccess,
}: {
category: PluginCategoryEnum
currentReferenceSetting?: ReferenceSetting
onSuccess?: () => void
}) => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (payload: ReferenceSetting) => {
const mutations: Array<Promise<unknown>> = []
if (!arePermissionsEqual(payload.permission, currentReferenceSetting?.permission))
mutations.push(
post('/workspaces/current/plugin/permission/change', { body: payload.permission }),
)
if (
!areAutoUpgradeSettingsEqual(payload.auto_upgrade, currentReferenceSetting?.auto_upgrade)
) {
mutations.push(
post('/workspaces/current/plugin/auto-upgrade/change', {
body: {
category,
auto_upgrade: payload.auto_upgrade,
},
}),
)
}
return Promise.all(mutations)
},
onMutate: async (payload) => {
const shouldUpdatePermission = !arePermissionsEqual(
payload.permission,
currentReferenceSetting?.permission,
)
const shouldUpdateAutoUpgrade = !areAutoUpgradeSettingsEqual(
payload.auto_upgrade,
currentReferenceSetting?.auto_upgrade,
)
const autoUpgradeQueryKey = pluginAutoUpgradeSettingsQueryKey(category)
await Promise.all([
shouldUpdatePermission
? queryClient.cancelQueries({ queryKey: usePluginPermissionSettingsKey })
: Promise.resolve(),
shouldUpdateAutoUpgrade
? queryClient.cancelQueries({ queryKey: autoUpgradeQueryKey })
: Promise.resolve(),
])
const previousPermission = queryClient.getQueryData<Permissions>(
usePluginPermissionSettingsKey,
)
const previousAutoUpgrade =
queryClient.getQueryData<PluginAutoUpgradeSettingsResponse>(autoUpgradeQueryKey)
const hadPreviousPermission = previousPermission !== undefined
const hadPreviousAutoUpgrade = previousAutoUpgrade !== undefined
if (shouldUpdatePermission)
queryClient.setQueryData(usePluginPermissionSettingsKey, payload.permission)
if (shouldUpdateAutoUpgrade) {
queryClient.setQueryData(autoUpgradeQueryKey, {
category,
auto_upgrade: payload.auto_upgrade,
} satisfies PluginAutoUpgradeSettingsResponse)
}
return {
previousPermission,
previousAutoUpgrade,
hadPreviousPermission,
hadPreviousAutoUpgrade,
shouldUpdatePermission,
shouldUpdateAutoUpgrade,
}
},
onError: (_error, _payload, context) => {
if (context?.shouldUpdatePermission && context.hadPreviousPermission)
queryClient.setQueryData(usePluginPermissionSettingsKey, context.previousPermission)
else if (context?.shouldUpdatePermission)
queryClient.removeQueries({ queryKey: usePluginPermissionSettingsKey })
if (context?.shouldUpdateAutoUpgrade && context.hadPreviousAutoUpgrade)
queryClient.setQueryData(
pluginAutoUpgradeSettingsQueryKey(category),
context.previousAutoUpgrade,
)
else if (context?.shouldUpdateAutoUpgrade)
queryClient.removeQueries({ queryKey: pluginAutoUpgradeSettingsQueryKey(category) })
},
onSuccess,
})
}
export const useRemoveAutoUpgrade = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (payload: { plugin_id: string; category: PluginCategoryEnum }) => {
return post('/workspaces/current/plugin/auto-upgrade/exclude', { body: payload })
},
onSuccess: (_data, payload) => {
queryClient.invalidateQueries({
queryKey: pluginAutoUpgradeSettingsQueryKey(payload.category),
})
},
})
}
export const useFetchPluginsInMarketPlaceByIds = (
unique_identifiers: string[],
options?: QueryOptions<{ data: PluginsFromMarketplaceResponse }>,
) => {
return useQuery({
...options,
queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByIds', unique_identifiers],
queryFn: () =>
postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/identifier/batch', {
body: {
unique_identifiers,
},
}),
enabled: unique_identifiers?.filter((i) => !!i).length > 0,
retry: 0,
})
}
export const useFetchPluginsInMarketPlaceByInfo = (infos: MarketplacePluginInfoRequest[]) => {
return useQuery({
queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByInfo', infos],
queryFn: () =>
postMarketplace<{ data: PluginsFromMarketplaceByInfoResponse }>('/plugins/versions/batch', {
body: {
plugin_tuples: infos.map((info) => ({
org: info.organization,
name: info.plugin,
version: info.version,
})),
},
}),
enabled: infos?.filter((i) => !!i).length > 0,
retry: 0,
})
}
export const usePluginTaskList = (category?: PluginCategoryEnum | string) => {
const initializedRef = useRef(false)
const queryClient = useQueryClient()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManagement = hasPermission(workspacePermissionKeys, 'plugin.install')
const { refreshPluginList } = useRefreshPluginList()
const query = useQuery<PluginTaskListResponse>({
enabled: canManagement,
queryKey: usePluginTaskListKey,
queryFn: () =>
get<{ tasks: PluginTask[] }>('/workspaces/current/plugin/tasks?page=1&page_size=100'),
structuralSharing: (previousData, nextData) =>
preserveLocalUnfinishedPluginTasks(
previousData as PluginTaskListResponse | undefined,
nextData as PluginTaskListResponse,
),
refetchInterval: (lastQuery) => {
const lastData = lastQuery.state.data
const taskDone = lastData?.tasks.every(
(task) => task.status === TaskStatus.success || task.status === TaskStatus.failed,
)
return taskDone ? false : 5000
},
})
const { data, isFetched, isRefetching, refetch } = query
useEffect(() => {
// After first fetch, refresh plugin list each time all tasks are done
// Skip initialization period, because the query cache is not updated yet
if (!initializedRef.current) {
initializedRef.current = true
return
}
if (isRefetching) return
const lastData = cloneDeep(data)
const taskDone = lastData?.tasks.every(
(task) => task.status === TaskStatus.success || task.status === TaskStatus.failed,
)
const taskAllFailed = lastData?.tasks.every((task) => task.status === TaskStatus.failed)
if (taskDone && lastData?.tasks.length && !taskAllFailed)
refreshPluginList(category ? { category } : undefined, !category)
}, [category, data, isRefetching, refreshPluginList])
const handleRefetch = useCallback(() => {
refetch()
}, [refetch])
const handleInstallTaskStart = useCallback(
(response: InstallPackageResponse) => {
if (response.all_installed) return
upsertStartedPluginTask(queryClient, response)
refetch()
},
[queryClient, refetch],
)
return {
data,
pluginTasks: data?.tasks || [],
isFetched,
handleRefetch,
handleInstallTaskStart,
}
}
export const useMutationClearTaskPlugin = () => {
return useMutation({
mutationFn: ({ taskId, pluginId }: { taskId: string; pluginId: string }) => {
const encodedPluginId = encodeURIComponent(pluginId)
return post<{ success: boolean }>(
`/workspaces/current/plugin/tasks/${taskId}/delete/${encodedPluginId}`,
)
},
})
}
export const usePluginManifestInfo = (pluginUID: string) => {
return useQuery({
enabled: !!pluginUID,
queryKey: [NAME_SPACE, 'manifest', pluginUID],
queryFn: () =>
getMarketplace<{ data: { plugin: PluginInfoFromMarketPlace; version: { version: string } } }>(
`/plugins/${pluginUID}`,
),
retry: 0,
})
}
export const useMutationCheckDependencies = () => {
return useMutation({
mutationFn: (appId: string) => {
return get<{ leaked_dependencies: Dependency[] }>(`/apps/imports/${appId}/check-dependencies`)
},
})
}
export const useModelInList = (currentProvider?: ModelProvider, modelId?: string) => {
const provider = currentProvider?.provider
return useQuery({
queryKey: ['modelInList', provider, modelId],
queryFn: async () => {
if (!modelId || !provider) return false
try {
const modelsData = await fetchModelProviderModelList(
`/workspaces/current/model-providers/${provider}/models`,
)
return !!modelId && modelsData.data.some((item) => item.model === modelId)
} catch {
return false
}
},
enabled: !!modelId && !!provider,
})
}
export const usePluginInfo = (providerName?: string) => {
return useQuery({
queryKey: ['pluginInfo', providerName],
queryFn: async () => {
if (!providerName) return null
const parts = providerName.split('/')
const org = parts[0]
const name = parts[1]
try {
const response = await fetchPluginInfoFromMarketPlace({ org: org!, name: name! })
return response.data.plugin.category === PluginCategoryEnum.model
? response.data.plugin
: null
} catch {
return null
}
},
enabled: !!providerName,
})
}
export const useFetchDynamicOptions = (
plugin_id: string,
provider: string,
action: string,
parameter: string,
provider_type?: string,
extra?: Record<string, unknown>,
) => {
return useMutation({
mutationFn: () =>
get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', {
params: {
plugin_id,
provider,
action,
parameter,
provider_type,
...extra,
},
}),
})
}
export const usePluginReadme = ({
plugin_unique_identifier,
language,
}: {
plugin_unique_identifier: string
language?: string
}) => {
return useQuery({
queryKey: ['pluginReadme', plugin_unique_identifier, language],
queryFn: () =>
get<{ readme: string }>(
'/workspaces/current/plugin/readme',
{ params: { plugin_unique_identifier, language } },
{ silent: true },
),
enabled: !!plugin_unique_identifier,
retry: 0,
})
}
export const usePluginReadmeAsset = ({
file_name,
plugin_unique_identifier,
}: {
file_name?: string
plugin_unique_identifier?: string
}) => {
const normalizedFileName = file_name?.replace(/^\.\/_assets\//, '').replace(/^_assets\//, '')
const isAssetFile = file_name?.startsWith('./_assets') || file_name?.startsWith('_assets')
return useQuery({
queryKey: ['pluginReadmeAsset', plugin_unique_identifier, normalizedFileName],
queryFn: () =>
get<Blob>(
'/workspaces/current/plugin/asset',
{ params: { plugin_unique_identifier, file_name: normalizedFileName } },
{ silent: true },
),
enabled: !!plugin_unique_identifier && !!isAssetFile,
})
}