mirror of
https://github.com/langgenius/dify.git
synced 2026-07-15 01:17:04 +08:00
refactor(web): migrate plugin console contracts (#38252)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -319,7 +319,7 @@ class PluginInstallationItemResponse(ResponseModel):
|
||||
plugin_unique_identifier: str
|
||||
version: str
|
||||
checksum: str
|
||||
declaration: Mapping[str, Any]
|
||||
declaration: PluginDeclarationResponse
|
||||
|
||||
|
||||
class PluginInstallationsResponse(ResponseModel):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, computed_field, model_validator
|
||||
|
||||
@ -32,7 +32,9 @@ class MarketplacePluginDeclaration(BaseModel):
|
||||
latest_package_identifier: str = Field(
|
||||
..., description="Unique identifier for the latest package release of the plugin"
|
||||
)
|
||||
status: str = Field(..., description="Indicate the status of marketplace plugin, enum from `active` `deleted`")
|
||||
status: Literal["active", "deleted"] = Field(
|
||||
..., description="Indicate the status of marketplace plugin, enum from `active` `deleted`"
|
||||
)
|
||||
deprecated_reason: str = Field(
|
||||
..., description="Not empty when status='deleted', indicates the reason why this plugin is deleted(deprecated)"
|
||||
)
|
||||
|
||||
@ -16,7 +16,7 @@ import logging
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from mimetypes import guess_type
|
||||
from typing import Any, ClassVar
|
||||
from typing import Any, ClassVar, Literal
|
||||
|
||||
from pydantic import BaseModel, TypeAdapter, ValidationError
|
||||
from redis import RedisError
|
||||
@ -75,7 +75,7 @@ class PluginService:
|
||||
plugin_id: str
|
||||
version: str
|
||||
unique_identifier: str
|
||||
status: str
|
||||
status: Literal["active", "deleted"]
|
||||
deprecated_reason: str
|
||||
alternative_plugin_id: str
|
||||
|
||||
|
||||
@ -18093,7 +18093,7 @@ Enum class for large language model mode.
|
||||
| alternative_plugin_id | string | | Yes |
|
||||
| deprecated_reason | string | | Yes |
|
||||
| plugin_id | string | | Yes |
|
||||
| status | string | | Yes |
|
||||
| status | string, <br>**Available values:** "active", "deleted" | *Enum:* `"active"`, `"deleted"` | Yes |
|
||||
| unique_identifier | string | | Yes |
|
||||
| version | string | | Yes |
|
||||
|
||||
@ -19590,7 +19590,7 @@ Shared permission levels for resources (datasets, credentials, etc.)
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| checksum | string | | Yes |
|
||||
| created_at | dateTime | | Yes |
|
||||
| declaration | object | | Yes |
|
||||
| declaration | [PluginDeclarationResponse](#plugindeclarationresponse) | | Yes |
|
||||
| endpoints_active | integer | | Yes |
|
||||
| endpoints_setups | integer | | Yes |
|
||||
| id | string | | Yes |
|
||||
|
||||
@ -42,6 +42,7 @@ from controllers.console.workspace.plugin import (
|
||||
PluginUploadFromGithubApi,
|
||||
PluginUploadFromPkgApi,
|
||||
)
|
||||
from core.plugin.entities.plugin import PluginInstallation
|
||||
from core.plugin.impl.exc import PluginDaemonClientSideError
|
||||
from models.account import Account, TenantAccountRole, TenantPluginAutoUpgradeStrategy, TenantPluginPermission
|
||||
|
||||
@ -478,12 +479,23 @@ class TestPluginListInstallationsFromIdsApi:
|
||||
app.test_request_context("/", json=payload),
|
||||
patch(
|
||||
"controllers.console.workspace.plugin.PluginService.list_installations_from_ids",
|
||||
return_value=[{"id": "p1"}],
|
||||
return_value=[PluginInstallation.model_validate(_plugin_category_list_item())],
|
||||
),
|
||||
):
|
||||
result = method(api, "t1")
|
||||
|
||||
assert "plugins" in result
|
||||
assert result["plugins"][0]["id"] == "entity-1"
|
||||
assert result["plugins"][0]["plugin_id"] == "test-author/test-plugin"
|
||||
assert result["plugins"][0]["plugin_unique_identifier"] == "test-author/test-plugin:1.0.0@checksum"
|
||||
assert result["plugins"][0]["version"] == "1.0.0"
|
||||
assert result["plugins"][0]["declaration"]["name"] == "test-plugin"
|
||||
assert "name" not in result["plugins"][0]
|
||||
assert "installation_id" not in result["plugins"][0]
|
||||
assert "latest_version" not in result["plugins"][0]
|
||||
assert "latest_unique_identifier" not in result["plugins"][0]
|
||||
assert "status" not in result["plugins"][0]
|
||||
assert "deprecated_reason" not in result["plugins"][0]
|
||||
assert "alternative_plugin_id" not in result["plugins"][0]
|
||||
|
||||
def test_daemon_error(self, app: Flask):
|
||||
api = PluginListInstallationsFromIdsApi()
|
||||
|
||||
@ -1134,9 +1134,7 @@ export type PluginAutoUpgradeSettingsResponseModel = {
|
||||
export type PluginInstallationItemResponse = {
|
||||
checksum: string
|
||||
created_at: string
|
||||
declaration: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
declaration: PluginDeclarationResponse
|
||||
endpoints_active: number
|
||||
endpoints_setups: number
|
||||
id: string
|
||||
@ -1156,7 +1154,7 @@ export type LatestPluginCache = {
|
||||
alternative_plugin_id: string
|
||||
deprecated_reason: string
|
||||
plugin_id: string
|
||||
status: string
|
||||
status: 'active' | 'deleted'
|
||||
unique_identifier: string
|
||||
version: string
|
||||
}
|
||||
@ -1500,39 +1498,6 @@ export type StrategySetting = 'disabled' | 'fix_only' | 'latest'
|
||||
|
||||
export type UpgradeMode = 'all' | 'exclude' | 'partial'
|
||||
|
||||
export type PluginInstallationSource = 'github' | 'marketplace' | 'package' | 'remote'
|
||||
|
||||
export type CoreToolsEntitiesCommonEntitiesI18nObject = {
|
||||
en_US: string
|
||||
ja_JP?: string | null
|
||||
pt_BR?: string | null
|
||||
zh_Hans?: string | null
|
||||
}
|
||||
|
||||
export type PluginCategoryBuiltinToolResponse = {
|
||||
author: string
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
labels: Array<string>
|
||||
name: string
|
||||
output_schema: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
parameters?: Array<{
|
||||
[key: string]: unknown
|
||||
}> | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type ToolProviderType
|
||||
= | 'api'
|
||||
| 'app'
|
||||
| 'builtin'
|
||||
| 'dataset-retrieval'
|
||||
| 'mcp'
|
||||
| 'plugin'
|
||||
| 'workflow'
|
||||
|
||||
export type PluginDeclarationResponse = {
|
||||
agent_strategy?: {
|
||||
[key: string]: unknown
|
||||
@ -1573,6 +1538,39 @@ export type PluginDeclarationResponse = {
|
||||
version: string
|
||||
}
|
||||
|
||||
export type PluginInstallationSource = 'github' | 'marketplace' | 'package' | 'remote'
|
||||
|
||||
export type CoreToolsEntitiesCommonEntitiesI18nObject = {
|
||||
en_US: string
|
||||
ja_JP?: string | null
|
||||
pt_BR?: string | null
|
||||
zh_Hans?: string | null
|
||||
}
|
||||
|
||||
export type PluginCategoryBuiltinToolResponse = {
|
||||
author: string
|
||||
description: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
label: CoreToolsEntitiesCommonEntitiesI18nObject
|
||||
labels: Array<string>
|
||||
name: string
|
||||
output_schema: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
parameters?: Array<{
|
||||
[key: string]: unknown
|
||||
}> | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type ToolProviderType
|
||||
= | 'api'
|
||||
| 'app'
|
||||
| 'builtin'
|
||||
| 'dataset-retrieval'
|
||||
| 'mcp'
|
||||
| 'plugin'
|
||||
| 'workflow'
|
||||
|
||||
export type RbacRoleAccount = {
|
||||
account_id: string
|
||||
account_name?: string
|
||||
|
||||
@ -1101,7 +1101,7 @@ export const zLatestPluginCache = z.object({
|
||||
alternative_plugin_id: z.string(),
|
||||
deprecated_reason: z.string(),
|
||||
plugin_id: z.string(),
|
||||
status: z.string(),
|
||||
status: z.enum(['active', 'deleted']),
|
||||
unique_identifier: z.string(),
|
||||
version: z.string(),
|
||||
})
|
||||
@ -1716,33 +1716,6 @@ export const zPluginAutoUpgradeFetchResponse = z.object({
|
||||
*/
|
||||
export const zPluginInstallationSource = z.enum(['github', 'marketplace', 'package', 'remote'])
|
||||
|
||||
/**
|
||||
* PluginInstallationItemResponse
|
||||
*/
|
||||
export const zPluginInstallationItemResponse = z.object({
|
||||
checksum: z.string(),
|
||||
created_at: z.iso.datetime(),
|
||||
declaration: z.record(z.string(), z.unknown()),
|
||||
endpoints_active: z.int(),
|
||||
endpoints_setups: z.int(),
|
||||
id: z.string(),
|
||||
meta: z.record(z.string(), z.unknown()),
|
||||
plugin_id: z.string(),
|
||||
plugin_unique_identifier: z.string(),
|
||||
runtime_type: z.string(),
|
||||
source: zPluginInstallationSource,
|
||||
tenant_id: z.string(),
|
||||
updated_at: z.iso.datetime(),
|
||||
version: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginInstallationsResponse
|
||||
*/
|
||||
export const zPluginInstallationsResponse = z.object({
|
||||
plugins: z.array(zPluginInstallationItemResponse),
|
||||
})
|
||||
|
||||
/**
|
||||
* I18nObject
|
||||
*
|
||||
@ -2348,6 +2321,33 @@ export const zPluginDeclarationResponse = z.object({
|
||||
version: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginInstallationItemResponse
|
||||
*/
|
||||
export const zPluginInstallationItemResponse = z.object({
|
||||
checksum: z.string(),
|
||||
created_at: z.iso.datetime(),
|
||||
declaration: zPluginDeclarationResponse,
|
||||
endpoints_active: z.int(),
|
||||
endpoints_setups: z.int(),
|
||||
id: z.string(),
|
||||
meta: z.record(z.string(), z.unknown()),
|
||||
plugin_id: z.string(),
|
||||
plugin_unique_identifier: z.string(),
|
||||
runtime_type: z.string(),
|
||||
source: zPluginInstallationSource,
|
||||
tenant_id: z.string(),
|
||||
updated_at: z.iso.datetime(),
|
||||
version: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginInstallationsResponse
|
||||
*/
|
||||
export const zPluginInstallationsResponse = z.object({
|
||||
plugins: z.array(zPluginInstallationItemResponse),
|
||||
})
|
||||
|
||||
/**
|
||||
* PluginCategoryInstalledPluginResponse
|
||||
*/
|
||||
|
||||
@ -84,6 +84,9 @@ vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useCheckInstalled: () => ({
|
||||
data: { plugins: [] },
|
||||
}),
|
||||
usePluginAutoUpgradeSettings: () => ({
|
||||
data: {
|
||||
category: 'model',
|
||||
@ -111,25 +114,40 @@ vi.mock('@/app/components/plugins/reference-setting-modal', () => ({
|
||||
|
||||
vi.mock('@/service/client', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/client')>()
|
||||
const originalPlugins = actual.consoleQuery.plugins as unknown as Record<string, unknown>
|
||||
const originalWorkspaces = actual.consoleQuery.workspaces
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery: new Proxy(actual.consoleQuery, {
|
||||
get(target, prop) {
|
||||
if (prop === 'plugins') {
|
||||
if (prop === 'workspaces') {
|
||||
return {
|
||||
...originalPlugins,
|
||||
checkInstalled: {
|
||||
queryOptions: () => ({
|
||||
queryKey: ['plugins', 'checkInstalled'],
|
||||
queryFn: () => new Promise(() => {}),
|
||||
}),
|
||||
},
|
||||
latestVersions: {
|
||||
queryOptions: () => ({
|
||||
queryKey: ['plugins', 'latestVersions'],
|
||||
queryFn: () => new Promise(() => {}),
|
||||
}),
|
||||
...originalWorkspaces,
|
||||
current: {
|
||||
...originalWorkspaces.current,
|
||||
plugin: {
|
||||
...originalWorkspaces.current.plugin,
|
||||
list: {
|
||||
...originalWorkspaces.current.plugin.list,
|
||||
installations: {
|
||||
ids: {
|
||||
post: {
|
||||
queryOptions: () => ({
|
||||
queryKey: ['workspaces', 'current', 'plugin', 'list', 'installations', 'ids', 'post'],
|
||||
queryFn: () => new Promise(() => {}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
latestVersions: {
|
||||
post: {
|
||||
queryOptions: () => ({
|
||||
queryKey: ['workspaces', 'current', 'plugin', 'list', 'latestVersions', 'post'],
|
||||
queryFn: () => new Promise(() => {}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,6 +163,9 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
useInstalledPluginList: () => ({
|
||||
data: { plugins: [] },
|
||||
}),
|
||||
useCheckInstalled: () => ({
|
||||
data: { plugins: [] },
|
||||
}),
|
||||
usePluginAutoUpgradeSettings: () => ({
|
||||
data: mockReferenceSetting.auto_upgrade
|
||||
? {
|
||||
@ -230,25 +233,40 @@ vi.mock('@/app/components/base/date-and-time-picker/time-picker', () => ({
|
||||
|
||||
vi.mock('@/service/client', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/client')>()
|
||||
const originalPlugins = actual.consoleQuery.plugins as unknown as Record<string, unknown>
|
||||
const originalWorkspaces = actual.consoleQuery.workspaces
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery: new Proxy(actual.consoleQuery, {
|
||||
get(target, prop) {
|
||||
if (prop === 'plugins') {
|
||||
if (prop === 'workspaces') {
|
||||
return {
|
||||
...originalPlugins,
|
||||
checkInstalled: {
|
||||
queryOptions: () => ({
|
||||
queryKey: ['plugins', 'checkInstalled'],
|
||||
queryFn: () => new Promise(() => {}),
|
||||
}),
|
||||
},
|
||||
latestVersions: {
|
||||
queryOptions: () => ({
|
||||
queryKey: ['plugins', 'latestVersions'],
|
||||
queryFn: () => new Promise(() => {}),
|
||||
}),
|
||||
...originalWorkspaces,
|
||||
current: {
|
||||
...originalWorkspaces.current,
|
||||
plugin: {
|
||||
...originalWorkspaces.current.plugin,
|
||||
list: {
|
||||
...originalWorkspaces.current.plugin.list,
|
||||
installations: {
|
||||
ids: {
|
||||
post: {
|
||||
queryOptions: () => ({
|
||||
queryKey: ['workspaces', 'current', 'plugin', 'list', 'installations', 'ids', 'post'],
|
||||
queryFn: () => new Promise(() => {}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
latestVersions: {
|
||||
post: {
|
||||
queryOptions: () => ({
|
||||
queryKey: ['workspaces', 'current', 'plugin', 'list', 'latestVersions', 'post'],
|
||||
queryFn: () => new Promise(() => {}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
ModelProvider,
|
||||
} from './declarations'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useMemo } from 'react'
|
||||
@ -14,7 +14,7 @@ import { usePluginSettingsAccess } from '@/app/components/plugins/plugin-page/us
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useCheckInstalled } from '@/service/use-plugins'
|
||||
import UpdateSettingDialog from '../update-setting-dialog'
|
||||
import {
|
||||
CustomConfigurationStatusEnum,
|
||||
@ -64,11 +64,10 @@ const ModelProviderPage = ({
|
||||
const allPluginIds = useMemo(() => {
|
||||
return [...new Set(providers.map(p => providerToPluginId(p.provider)).filter(Boolean))]
|
||||
}, [providers])
|
||||
const { data: installedPlugins } = useQuery(consoleQuery.plugins.checkInstalled.queryOptions({
|
||||
input: { body: { plugin_ids: allPluginIds } },
|
||||
const { data: installedPlugins } = useCheckInstalled({
|
||||
pluginIds: allPluginIds,
|
||||
enabled: allPluginIds.length > 0,
|
||||
staleTime: 0,
|
||||
}))
|
||||
})
|
||||
const enrichedPlugins = usePluginsWithLatestVersion(installedPlugins?.plugins)
|
||||
const pluginDetailMap = useMemo(() => {
|
||||
const map = new Map<string, PluginDetail>()
|
||||
|
||||
@ -11,9 +11,17 @@ vi.mock('@tanstack/react-query', () => ({
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
plugins: {
|
||||
latestVersions: {
|
||||
queryOptions: vi.fn((options: unknown) => options),
|
||||
workspaces: {
|
||||
current: {
|
||||
plugin: {
|
||||
list: {
|
||||
latestVersions: {
|
||||
post: {
|
||||
queryOptions: vi.fn((options: unknown) => options),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -55,7 +63,7 @@ describe('usePluginsWithLatestVersion', () => {
|
||||
|
||||
const { result } = renderHook(() => usePluginsWithLatestVersion(plugins))
|
||||
|
||||
expect(consoleQuery.plugins.latestVersions.queryOptions).toHaveBeenCalledWith({
|
||||
expect(consoleQuery.workspaces.current.plugin.list.latestVersions.post.queryOptions).toHaveBeenCalledWith({
|
||||
input: { body: { plugin_ids: [] } },
|
||||
enabled: false,
|
||||
})
|
||||
@ -109,7 +117,7 @@ describe('usePluginsWithLatestVersion', () => {
|
||||
|
||||
const { result } = renderHook(() => usePluginsWithLatestVersion(plugins))
|
||||
|
||||
expect(consoleQuery.plugins.latestVersions.queryOptions).toHaveBeenCalledWith({
|
||||
expect(consoleQuery.workspaces.current.plugin.list.latestVersions.post.queryOptions).toHaveBeenCalledWith({
|
||||
input: { body: { plugin_ids: ['plugin-1'] } },
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
@ -109,7 +109,7 @@ export function usePluginsWithLatestVersion(plugins: PluginDetail[] = EMPTY_PLUG
|
||||
[plugins],
|
||||
)
|
||||
|
||||
const { data: latestVersionData } = useQuery(consoleQuery.plugins.latestVersions.queryOptions({
|
||||
const { data: latestVersionData } = useQuery(consoleQuery.workspaces.current.plugin.list.latestVersions.post.queryOptions({
|
||||
input: { body: { plugin_ids: marketplacePluginIds } },
|
||||
enabled: !!marketplacePluginIds.length,
|
||||
}))
|
||||
|
||||
@ -450,18 +450,6 @@ export type InstalledPluginCategoryListResponse = {
|
||||
has_more: boolean
|
||||
}
|
||||
|
||||
export type InstalledLatestVersionResponse = {
|
||||
versions: {
|
||||
[plugin_id: string]: {
|
||||
unique_identifier: string
|
||||
version: string
|
||||
status: 'active' | 'deleted'
|
||||
deprecated_reason: string
|
||||
alternative_plugin_id: string
|
||||
} | null
|
||||
}
|
||||
}
|
||||
|
||||
export type UninstallPluginResponse = {
|
||||
success: boolean
|
||||
}
|
||||
|
||||
@ -158,3 +158,7 @@ export const exploreRouterContract = {
|
||||
installedAppMeta: exploreInstalledAppMetaContract,
|
||||
banners: exploreBannersContract,
|
||||
}
|
||||
|
||||
export const exploreConsoleRouterContract = {
|
||||
explore: exploreRouterContract,
|
||||
}
|
||||
|
||||
@ -1,32 +1 @@
|
||||
import type { InstalledLatestVersionResponse, PluginDetail } from '@/app/components/plugins/types'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
export const pluginCheckInstalledContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/plugin/list/installations/ids',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: {
|
||||
plugin_ids: string[]
|
||||
}
|
||||
}>())
|
||||
.output(type<{ plugins: PluginDetail[] }>())
|
||||
|
||||
export const pluginLatestVersionsContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/plugin/list/latest-versions',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: {
|
||||
plugin_ids: string[]
|
||||
}
|
||||
}>())
|
||||
.output(type<InstalledLatestVersionResponse>())
|
||||
|
||||
export const pluginsRouterContract = {
|
||||
checkInstalled: pluginCheckInstalledContract,
|
||||
latestVersions: pluginLatestVersionsContract,
|
||||
}
|
||||
export const pluginsConsoleRouterContract = {}
|
||||
|
||||
@ -370,3 +370,7 @@ export const snippetsRouterContract = {
|
||||
runDraftWorkflow: runSnippetDraftWorkflowContract,
|
||||
stopWorkflowTask: stopSnippetWorkflowTaskContract,
|
||||
}
|
||||
|
||||
export const snippetsConsoleRouterContract = {
|
||||
snippets: snippetsRouterContract,
|
||||
}
|
||||
|
||||
@ -63,3 +63,7 @@ export const trialAppsRouterContract = {
|
||||
parameters: trialAppParametersContract,
|
||||
workflows: trialAppWorkflowsContract,
|
||||
}
|
||||
|
||||
export const trialAppsConsoleRouterContract = {
|
||||
trialApps: trialAppsRouterContract,
|
||||
}
|
||||
|
||||
@ -45,10 +45,10 @@ import { workflowGenerate } from '@dify/contracts/api/console/workflow-generate/
|
||||
import { workflow } from '@dify/contracts/api/console/workflow/orpc.gen'
|
||||
import { workspaces } from '@dify/contracts/api/console/workspaces/orpc.gen'
|
||||
import { contract as enterpriseContract } from '@dify/contracts/enterprise/orpc.gen'
|
||||
import { exploreRouterContract } from './console/explore'
|
||||
import { pluginsRouterContract } from './console/plugins'
|
||||
import { snippetsRouterContract } from './console/snippets'
|
||||
import { trialAppsRouterContract } from './console/try-app'
|
||||
import { exploreConsoleRouterContract } from './console/explore'
|
||||
import { pluginsConsoleRouterContract } from './console/plugins'
|
||||
import { snippetsConsoleRouterContract } from './console/snippets'
|
||||
import { trialAppsConsoleRouterContract } from './console/try-app'
|
||||
|
||||
const communityContract = {
|
||||
account,
|
||||
@ -102,8 +102,8 @@ const communityContract = {
|
||||
export const consoleRouterContract = {
|
||||
enterprise: enterpriseContract,
|
||||
...communityContract,
|
||||
explore: exploreRouterContract,
|
||||
plugins: pluginsRouterContract,
|
||||
snippets: snippetsRouterContract,
|
||||
trialApps: trialAppsRouterContract,
|
||||
...exploreConsoleRouterContract,
|
||||
...pluginsConsoleRouterContract,
|
||||
...snippetsConsoleRouterContract,
|
||||
...trialAppsConsoleRouterContract,
|
||||
}
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import type { PluginInstallationItemResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Permissions, PluginTaskStart } from '@/app/components/plugins/types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import { PermissionType, PluginCategoryEnum, PluginSource, TaskStatus } from '@/app/components/plugins/types'
|
||||
import {
|
||||
normalizeInstalledPluginDetail,
|
||||
useInstalledPluginList,
|
||||
useMutationPluginAutoUpgradeSettings,
|
||||
useMutationPluginPermissionSettings,
|
||||
@ -67,6 +70,177 @@ const createWrapper = (queryClient: QueryClient) => {
|
||||
}
|
||||
}
|
||||
|
||||
const createPluginInstallation = (): PluginInstallationItemResponse => ({
|
||||
id: 'installation-row-id',
|
||||
created_at: '2026-06-01T00:00:00Z',
|
||||
updated_at: '2026-06-02T00:00:00Z',
|
||||
plugin_id: 'langgenius/full-declaration',
|
||||
plugin_unique_identifier: 'langgenius/full-declaration:1.0.0@test',
|
||||
tenant_id: 'tenant-id',
|
||||
endpoints_setups: 1,
|
||||
endpoints_active: 1,
|
||||
version: '1.0.0',
|
||||
source: PluginSource.marketplace,
|
||||
runtime_type: 'local',
|
||||
checksum: 'checksum',
|
||||
meta: {
|
||||
repo: 'langgenius/full-declaration',
|
||||
version: '1.0.0',
|
||||
package: 'full-declaration.difypkg',
|
||||
},
|
||||
declaration: {
|
||||
version: '1.0.0',
|
||||
author: 'Dify',
|
||||
icon: 'icon.svg',
|
||||
icon_dark: 'icon-dark.svg',
|
||||
name: 'full-declaration',
|
||||
category: PluginCategoryEnum.trigger,
|
||||
label: { en_US: 'Full declaration', zh_Hans: 'Full declaration' },
|
||||
description: { en_US: 'Full declaration plugin', zh_Hans: 'Full declaration plugin' },
|
||||
created_at: '2026-06-01T00:00:00Z',
|
||||
resource: {},
|
||||
plugins: {},
|
||||
verified: true,
|
||||
tags: ['automation'],
|
||||
meta: {
|
||||
version: '1.0.0',
|
||||
minimum_dify_version: '1.4.0',
|
||||
},
|
||||
tool: {
|
||||
identity: {
|
||||
author: 'Dify',
|
||||
name: 'tool-provider',
|
||||
description: { en_US: 'Tool provider' },
|
||||
icon: 'tool.svg',
|
||||
label: { en_US: 'Tool provider' },
|
||||
tags: ['tool'],
|
||||
},
|
||||
credentials_schema: [
|
||||
{
|
||||
name: 'api_key',
|
||||
label: { en_US: 'API key' },
|
||||
type: 'secret-input',
|
||||
required: true,
|
||||
default: 'token',
|
||||
},
|
||||
],
|
||||
},
|
||||
datasource: {
|
||||
identity: {
|
||||
author: 'Dify',
|
||||
name: 'datasource-provider',
|
||||
description: { en_US: 'Datasource provider' },
|
||||
icon: 'datasource.svg',
|
||||
label: { en_US: 'Datasource provider' },
|
||||
tags: ['datasource'],
|
||||
},
|
||||
credentials_schema: [],
|
||||
},
|
||||
endpoint: {
|
||||
settings: [
|
||||
{
|
||||
name: 'endpoint_secret',
|
||||
label: { en_US: 'Endpoint secret' },
|
||||
type: 'secret-input',
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
endpoints: [
|
||||
{
|
||||
path: '/webhook',
|
||||
method: 'POST',
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
trigger: {
|
||||
events: [
|
||||
{
|
||||
name: 'issue_created',
|
||||
identity: {
|
||||
author: 'Dify',
|
||||
name: 'issue_created',
|
||||
label: { en_US: 'Issue created' },
|
||||
provider: 'github',
|
||||
},
|
||||
description: { en_US: 'Issue created event' },
|
||||
parameters: [
|
||||
{
|
||||
name: 'retry_count',
|
||||
label: { en_US: 'Retry count' },
|
||||
type: 'number',
|
||||
default: 0,
|
||||
required: false,
|
||||
multiple: false,
|
||||
},
|
||||
],
|
||||
output_schema: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
],
|
||||
identity: {
|
||||
author: 'Dify',
|
||||
name: 'github-trigger',
|
||||
label: { en_US: 'GitHub trigger' },
|
||||
description: { en_US: 'GitHub trigger provider' },
|
||||
icon: 'trigger.svg',
|
||||
tags: ['trigger'],
|
||||
},
|
||||
subscription_constructor: {
|
||||
credentials_schema: [],
|
||||
oauth_schema: {
|
||||
client_schema: [],
|
||||
credentials_schema: [],
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
name: 'max_retries',
|
||||
label: { en_US: 'Max retries' },
|
||||
type: 'number',
|
||||
default: 3,
|
||||
required: false,
|
||||
multiple: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
subscription_schema: [
|
||||
{
|
||||
name: 'repository',
|
||||
label: { en_US: 'Repository' },
|
||||
type: 'text-input',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
describe('normalizeInstalledPluginDetail', () => {
|
||||
it('should preserve generated plugin declaration capabilities', () => {
|
||||
const detail = normalizeInstalledPluginDetail(createPluginInstallation())
|
||||
|
||||
expect(detail.name).toBe('full-declaration')
|
||||
expect(detail.installation_id).toBe('installation-row-id')
|
||||
expect(detail.latest_version).toBe('1.0.0')
|
||||
expect(detail.latest_unique_identifier).toBe('langgenius/full-declaration:1.0.0@test')
|
||||
expect(detail.status).toBe('active')
|
||||
expect(detail.declaration.tool?.identity.name).toBe('tool-provider')
|
||||
expect(detail.declaration.tool?.credentials_schema[0]?.name).toBe('api_key')
|
||||
expect(detail.declaration.datasource?.identity.name).toBe('datasource-provider')
|
||||
expect(detail.declaration.endpoint?.endpoints?.[0]).toEqual({
|
||||
path: '/webhook',
|
||||
method: 'POST',
|
||||
hidden: false,
|
||||
})
|
||||
expect(detail.declaration.trigger.identity.name).toBe('github-trigger')
|
||||
expect(detail.declaration.trigger.events[0]?.parameters[0]?.default).toBe(0)
|
||||
expect(detail.declaration.trigger.subscription_constructor.parameters[0]?.type).toBe(FormTypeEnum.textNumber)
|
||||
expect(detail.declaration.trigger.subscription_schema[0]?.name).toBe('repository')
|
||||
})
|
||||
})
|
||||
|
||||
describe('use-plugins mutations', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import type { AnyContractRouter } from '@orpc/contract'
|
||||
import { contractLoaders } from '@dify/contracts/api/console/orpc.gen'
|
||||
|
||||
const wrapConsoleContract = (segment: string, contract: unknown) => ({ [segment]: contract }) as AnyContractRouter
|
||||
type ConsoleContractExtension = Record<string, unknown>
|
||||
|
||||
const wrapConsoleContract = (segment: string, contract: unknown): ConsoleContractExtension => ({ [segment]: contract })
|
||||
|
||||
async function loadGeneratedConsoleContract(segment: string) {
|
||||
const loader = contractLoaders[segment as keyof typeof contractLoaders]
|
||||
@ -11,18 +13,21 @@ async function loadGeneratedConsoleContract(segment: string) {
|
||||
return loader() as Promise<AnyContractRouter>
|
||||
}
|
||||
|
||||
const customConsoleContractLoaders: Record<string, () => Promise<AnyContractRouter>> = {
|
||||
const customConsoleContractLoaders: Record<string, () => Promise<ConsoleContractExtension>> = {
|
||||
enterprise: () => import('@dify/contracts/enterprise/orpc.gen').then(({ contract }) => wrapConsoleContract('enterprise', contract)),
|
||||
explore: () => import('@/contract/console/explore').then(({ exploreRouterContract }) => wrapConsoleContract('explore', exploreRouterContract)),
|
||||
plugins: () => import('@/contract/console/plugins').then(({ pluginsRouterContract }) => wrapConsoleContract('plugins', pluginsRouterContract)),
|
||||
snippets: () => import('@/contract/console/snippets').then(({ snippetsRouterContract }) => wrapConsoleContract('snippets', snippetsRouterContract)),
|
||||
trialApps: () => import('@/contract/console/try-app').then(({ trialAppsRouterContract }) => wrapConsoleContract('trialApps', trialAppsRouterContract)),
|
||||
explore: () => import('@/contract/console/explore').then(({ exploreConsoleRouterContract }) => exploreConsoleRouterContract),
|
||||
plugins: () => import('@/contract/console/plugins').then(({ pluginsConsoleRouterContract }) => pluginsConsoleRouterContract),
|
||||
snippets: () => import('@/contract/console/snippets').then(({ snippetsConsoleRouterContract }) => snippetsConsoleRouterContract),
|
||||
trialApps: () => import('@/contract/console/try-app').then(({ trialAppsConsoleRouterContract }) => trialAppsConsoleRouterContract),
|
||||
}
|
||||
|
||||
export async function loadConsoleContractForSegment(segment: string) {
|
||||
const customContractLoader = customConsoleContractLoaders[segment]
|
||||
if (customContractLoader)
|
||||
return customContractLoader()
|
||||
if (customContractLoader) {
|
||||
const customContract = await customContractLoader()
|
||||
if (Object.keys(customContract).length > 0)
|
||||
return customContract as AnyContractRouter
|
||||
}
|
||||
|
||||
const generatedContract = await loadGeneratedConsoleContract(segment)
|
||||
if (generatedContract)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { PluginInstallationItemResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
||||
import type { MutateOptions, QueryClient, QueryOptions } from '@tanstack/react-query'
|
||||
import type {
|
||||
FormOption,
|
||||
@ -14,10 +15,12 @@ import type {
|
||||
InstalledPluginListWithTotalResponse,
|
||||
InstallPackageResponse,
|
||||
InstallStatusResponse,
|
||||
MetaData,
|
||||
PackageDependency,
|
||||
Permissions,
|
||||
Plugin,
|
||||
PluginDeclaration,
|
||||
PluginDetail,
|
||||
PluginInfoFromMarketPlace,
|
||||
PluginsFromMarketplaceByInfoResponse,
|
||||
PluginsFromMarketplaceResponse,
|
||||
@ -36,9 +39,10 @@ import {
|
||||
} from '@tanstack/react-query'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
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, TaskStatus } from '@/app/components/plugins/types'
|
||||
import { PluginCategoryEnum, PluginSource, TaskStatus } from '@/app/components/plugins/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchModelProviderModelList } from '@/service/common'
|
||||
import { fetchPluginInfoFromMarketPlace, uninstallPlugin } from '@/service/plugins'
|
||||
@ -56,6 +60,417 @@ 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 => ({
|
||||
@ -114,10 +529,13 @@ export const useCheckInstalled = ({
|
||||
pluginIds: string[]
|
||||
enabled: boolean
|
||||
}) => {
|
||||
return useQuery(consoleQuery.plugins.checkInstalled.queryOptions({
|
||||
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),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -125,7 +543,7 @@ export const useInvalidateCheckInstalled = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.plugins.checkInstalled.key(),
|
||||
queryKey: consoleQuery.workspaces.current.plugin.list.installations.ids.post.key(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user