refactor(web): move marketplace contract to contracts package (#38311)

This commit is contained in:
Stephen Zhou 2026-07-02 16:41:02 +08:00 committed by GitHub
parent e13271ba29
commit eb67c9c0ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 288 additions and 216 deletions

View File

@ -156,7 +156,7 @@ jobs:
- name: Generate frontend contracts
if: github.event_name != 'merge_group' && steps.frontend-contract-changes.outputs.any_changed == 'true'
run: pnpm --dir packages/contracts gen-api-contract-from-openapi
run: pnpm --dir packages/contracts gen-api-contract
- name: ESLint autofix
if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'

View File

@ -0,0 +1,233 @@
import type { InferContractRouterInputs } from '@orpc/contract'
import { oc, type } from '@orpc/contract'
// This Marketplace contract is manually maintained because these APIs are not generated from Dify OpenAPI specs.
const base = oc.$route({ inputStructure: 'detailed' })
export type SearchParamsFromCollection = {
query?: string
sort_by?: string
sort_order?: string
}
export type MarketplaceCollection = {
name: string
label: Record<string, string>
description: Record<string, string>
rule: string
created_at: string
updated_at: string
searchable?: boolean
search_params?: SearchParamsFromCollection
}
export type PluginsSearchParams = {
query: string
page?: number
page_size?: number
sort_by?: string
sort_order?: string
category?: string
tags?: string[]
exclude?: string[]
type?: 'plugin' | 'bundle'
}
export type PluginsSort = {
sortBy: string
sortOrder: string
}
export type CollectionsAndPluginsSearchParams = {
category?: string
condition?: string
exclude?: string[]
type?: 'plugin' | 'bundle'
}
export type MarketplaceTemplate = {
id: string
template_name: string
overview: string
icon: string
icon_background: string
icon_file_key: string
publisher_unique_handle: string
usage_count: number
categories: string[]
}
export type MarketplacePluginCategory
= | 'tool'
| 'model'
| 'extension'
| 'agent-strategy'
| 'datasource'
| 'trigger'
export type MarketplacePluginType
= | 'plugin'
| 'bundle'
| 'model'
| 'extension'
| 'tool'
| 'agent_strategy'
| 'datasource'
| 'trigger'
export type MarketplacePluginDependencySource = 'github' | 'marketplace' | 'package'
export type MarketplaceI18nObject = Partial<Record<string, string>>
export type MarketplacePlugin = {
type: MarketplacePluginType
org: string
author?: string
name: string
plugin_id: string
version: string
latest_version: string
latest_package_identifier: string
icon: string
icon_dark?: string
verified: boolean
label?: MarketplaceI18nObject
labels?: MarketplaceI18nObject
brief?: MarketplaceI18nObject | string
description?: MarketplaceI18nObject | string
introduction: string
repository: string
category: MarketplacePluginCategory
install_count: number
endpoint: {
settings: Array<Record<string, unknown>>
}
tags: Array<{ name: string }>
badges: string[] | null
verification: {
authorized_category: 'langgenius' | 'partner' | 'community'
}
from: MarketplacePluginDependencySource
}
export type PluginInfoFromMarketPlace = {
category: MarketplacePluginCategory
latest_package_identifier: string
latest_version: string
}
export type PluginsFromMarketplaceResponse = {
plugins: MarketplacePlugin[]
bundles?: MarketplacePlugin[]
total: number
}
export type PluginsFromMarketplaceByInfoResponse = {
list: Array<{
plugin: MarketplacePlugin
version: {
plugin_name: string
plugin_org: string
unique_identifier: string
}
}>
}
export type CollectionsResponse = {
data?: {
collections?: MarketplaceCollection[]
}
}
export type CollectionPluginsResponse = {
data?: {
plugins?: MarketplacePlugin[]
}
}
export type SearchAdvancedResponse = {
data: PluginsFromMarketplaceResponse
}
export type TemplateDetailResponse = {
data: MarketplaceTemplate
}
export type DownloadPluginResponse = Blob
const collectionsContract = base
.route({
path: '/collections',
method: 'GET',
})
.input(
type<{
query?: CollectionsAndPluginsSearchParams & { page?: number, page_size?: number }
}>(),
)
.output(type<CollectionsResponse>())
const collectionPluginsContract = base
.route({
path: '/collections/{collectionId}/plugins',
method: 'POST',
})
.input(
type<{
params: {
collectionId: string
}
body?: CollectionsAndPluginsSearchParams
}>(),
)
.output(type<CollectionPluginsResponse>())
const searchAdvancedContract = base
.route({
path: '/{kind}/search/advanced',
method: 'POST',
})
.input(type<{
params: {
kind: 'plugins' | 'bundles'
}
body: Omit<PluginsSearchParams, 'type'>
}>())
.output(type<SearchAdvancedResponse>())
const templateDetailContract = base
.route({
path: '/templates/{templateId}',
method: 'GET',
})
.input(type<{
params: {
templateId: string
}
}>())
.output(type<TemplateDetailResponse>())
const downloadPluginContract = base
.route({
path: '/plugins/{organization}/{pluginName}/{version}/download',
method: 'GET',
})
.input(type<{
params: {
organization: string
pluginName: string
version: string
}
}>())
.output(type<DownloadPluginResponse>())
export const marketplaceRouterContract = {
collections: collectionsContract,
collectionPlugins: collectionPluginsContract,
searchAdvanced: searchAdvancedContract,
templateDetail: templateDetailContract,
downloadPlugin: downloadPluginContract,
}
export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRouterContract>

View File

@ -4,6 +4,10 @@
"version": "0.0.0-private",
"private": true,
"exports": {
"./marketplace": {
"types": "./marketplace.ts",
"import": "./marketplace.ts"
},
"./api/*": {
"types": "./generated/api/*.ts",
"import": "./generated/api/*.ts"

View File

@ -17,7 +17,7 @@ vi.mock('../../../plugins/card/base/card-icon', () => ({
}))
vi.mock('../../../plugins/marketplace/utils', () => ({
getPluginIconInMarketplace: vi.fn(() => 'icon-url'),
getFormattedPlugin: vi.fn(plugin => ({ ...plugin, icon: 'icon-url' })),
}))
describe('pluginAction', () => {

View File

@ -1,9 +1,10 @@
import type { Plugin, PluginsFromMarketplaceResponse } from '../../plugins/types'
import type { PluginsFromMarketplaceResponse } from '@dify/contracts/marketplace'
import type { Plugin } from '../../plugins/types'
import type { ActionItem, PluginSearchResult } from './types'
import { renderI18nObject } from '@/i18n-config'
import { postMarketplace } from '@/service/base'
import Icon from '../../plugins/card/base/card-icon'
import { getPluginIconInMarketplace } from '../../plugins/marketplace/utils'
import { getFormattedPlugin } from '../../plugins/marketplace/utils'
const parser = (plugins: Plugin[], locale: string): PluginSearchResult[] => {
return plugins.map((plugin) => {
@ -39,10 +40,7 @@ export const pluginAction: ActionItem = {
return []
}
const list = response.data.plugins.map(plugin => ({
...plugin,
icon: getPluginIconInMarketplace(plugin),
}))
const list = response.data.plugins.map(plugin => getFormattedPlugin(plugin))
return parser(list, locale!)
}
catch (error) {

View File

@ -5,6 +5,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useState } from 'react'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { pluginInstallLimit } from '@/app/components/plugins/install-plugin/hooks/use-install-plugin-limit'
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
@ -154,11 +155,11 @@ export function useInstallMultiState({
const pluginInfo = (pluginId ? pluginById.get(pluginId) : undefined) || payloads[requestIndex]?.plugin
if (pluginInfo) {
pluginMap.set(request.dslIndex, {
pluginMap.set(request.dslIndex, getFormattedPlugin({
...pluginInfo,
from: request.dependency.type,
version: pluginInfo.version || pluginInfo.latest_version,
})
}))
}
else { errorSet.add(request.dslIndex) }
})

View File

@ -1,4 +1,4 @@
import type { PluginsSort, SearchParamsFromCollection } from './types'
import type { PluginsSort, SearchParamsFromCollection } from '@dify/contracts/marketplace'
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useQueryState } from 'nuqs'
import { useCallback } from 'react'

View File

@ -1,12 +1,12 @@
import type {
Plugin,
} from '../types'
import type {
CollectionsAndPluginsSearchParams,
MarketplaceCollection,
PluginsFromMarketplaceResponse,
PluginsSearchParams,
} from './types'
import type { PluginsFromMarketplaceResponse } from '@/app/components/plugins/types'
} from '@dify/contracts/marketplace'
import type {
Plugin,
} from '../types'
import {
useInfiniteQuery,
useQuery,

View File

@ -1,4 +1,4 @@
import type { MarketplaceCollection, SearchParamsFromCollection } from '../../types'
import type { MarketplaceCollection, SearchParamsFromCollection } from '@dify/contracts/marketplace'
import type { Plugin } from '@/app/components/plugins/types'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

View File

@ -1,4 +1,4 @@
import type { MarketplaceCollection } from '../../types'
import type { MarketplaceCollection } from '@dify/contracts/marketplace'
import type { Plugin } from '@/app/components/plugins/types'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

View File

@ -1,4 +1,4 @@
import type { MarketplaceCollection } from '../../types'
import type { MarketplaceCollection } from '@dify/contracts/marketplace'
import type { Plugin } from '@/app/components/plugins/types'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

View File

@ -1,6 +1,6 @@
'use client'
import type { MarketplaceCollection, SearchParamsFromCollection } from '@dify/contracts/marketplace'
import type { Plugin } from '../../types'
import type { MarketplaceCollection, SearchParamsFromCollection } from '../types'
import { cn } from '@langgenius/dify-ui/cn'
import { PluginInstallPermissionProviderGuard } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider'
import Empty from '../empty'

View File

@ -1,6 +1,6 @@
'use client'
import type { MarketplaceCollection, SearchParamsFromCollection } from '../types'
import type { MarketplaceCollection, SearchParamsFromCollection } from '@dify/contracts/marketplace'
import type { Plugin } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn'
import { useEffect, useMemo, useState } from 'react'

View File

@ -1,5 +1,4 @@
import type { PluginsSearchParams } from './types'
import type { MarketPlaceInputs } from '@/contract/marketplace'
import type { MarketPlaceInputs, PluginsSearchParams } from '@dify/contracts/marketplace'
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { marketplaceQuery } from '@/service/client'
import { getMarketplaceCollectionsAndPlugins, getMarketplacePlugins } from './utils'

View File

@ -1,4 +1,4 @@
import type { PluginsSearchParams } from './types'
import type { PluginsSearchParams } from '@dify/contracts/marketplace'
import { useDebounce } from 'ahooks'
import { useCallback, useMemo } from 'react'
import { useActivePluginType, useFilterPluginTags, useMarketplaceSearchMode, useMarketplaceSortValue, useSearchPluginText } from './atoms'

View File

@ -1,40 +0,0 @@
export type SearchParamsFromCollection = {
query?: string
sort_by?: string
sort_order?: string
}
export type MarketplaceCollection = {
name: string
label: Record<string, string>
description: Record<string, string>
rule: string
created_at: string
updated_at: string
searchable?: boolean
search_params?: SearchParamsFromCollection
}
export type PluginsSearchParams = {
query: string
page?: number
page_size?: number
sort_by?: string
sort_order?: string
category?: string
tags?: string[]
exclude?: string[]
type?: 'plugin' | 'bundle'
}
export type PluginsSort = {
sortBy: string
sortOrder: string
}
export type CollectionsAndPluginsSearchParams = {
category?: string
condition?: string
exclude?: string[]
type?: 'plugin' | 'bundle'
}

View File

@ -1,9 +1,10 @@
import type { ActivePluginType } from './constants'
import type {
CollectionsAndPluginsSearchParams,
MarketplaceCollection,
MarketplacePlugin,
PluginsSearchParams,
} from '@/app/components/plugins/marketplace/types'
} from '@dify/contracts/marketplace'
import type { ActivePluginType } from './constants'
import type { Plugin } from '@/app/components/plugins/types'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import {
@ -26,29 +27,32 @@ export function buildCarouselPages<T>(items: T[], itemsPerPage: number): T[][] {
return pages
}
export const getPluginIconInMarketplace = (plugin: Plugin) => {
type MarketplacePluginPayload = MarketplacePlugin | (Plugin & { labels?: Plugin['label'] })
export const getPluginIconInMarketplace = (plugin: Pick<MarketplacePluginPayload, 'name' | 'org' | 'type'>) => {
if (plugin.type === 'bundle')
return `${MARKETPLACE_API_PREFIX}/bundles/${plugin.org}/${plugin.name}/icon`
return `${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon`
}
export const getFormattedPlugin = (bundle: Plugin): Plugin => {
if (bundle.type === 'bundle') {
export const getFormattedPlugin = (payload: MarketplacePluginPayload): Plugin => {
const plugin = payload as unknown as Plugin
if (payload.type === 'bundle') {
return {
...bundle,
icon: getPluginIconInMarketplace(bundle),
brief: bundle.description,
// @ts-expect-error I do not have enough information
label: bundle.labels,
...plugin,
icon: getPluginIconInMarketplace(payload),
brief: payload.description as Plugin['brief'],
label: (payload.labels ?? payload.label) as Plugin['label'],
}
}
return {
...bundle,
icon: getPluginIconInMarketplace(bundle),
...plugin,
icon: getPluginIconInMarketplace(payload),
}
}
export const getPluginLinkInMarketplace = (plugin: Plugin, params?: Record<string, string | undefined>) => {
export const getPluginLinkInMarketplace = (plugin: Pick<MarketplacePluginPayload, 'name' | 'org' | 'type'>, params?: Record<string, string | undefined>) => {
if (plugin.type === 'bundle')
return getMarketplaceUrl(`/bundles/${plugin.org}/${plugin.name}`, params)
return getMarketplaceUrl(`/plugins/${plugin.org}/${plugin.name}`, params)

View File

@ -1,4 +1,5 @@
import type { PluginInfoFromMarketPlace, PluginStatus } from '@/app/components/plugins/types'
import type { PluginInfoFromMarketPlace } from '@dify/contracts/marketplace'
import type { PluginStatus } from '@/app/components/plugins/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { PluginCategoryEnum, PluginSource, TaskStatus } from '@/app/components/plugins/types'
import { fetchPluginInfoFromMarketPlace } from '@/service/plugins'

View File

@ -38,7 +38,7 @@ const ErrorPluginItem: FC<ErrorPluginItemProps> = ({ plugin, getIconUrl, languag
const manifest: Plugin = {
plugin_id: plugin.plugin_id,
type: info.category as Plugin['type'],
category: info.category,
category: info.category as Plugin['category'],
name: name!,
org: org!,
version: info.latest_version,

View File

@ -224,12 +224,6 @@ export type PluginDetail = {
alternative_plugin_id: string
}
export type PluginInfoFromMarketPlace = {
category: PluginCategoryEnum
latest_package_identifier: string
latest_version: string
}
export type Plugin = {
type: 'plugin' | 'bundle' | 'model' | 'extension' | 'tool' | 'agent_strategy' | 'datasource' | 'trigger'
org: string
@ -454,22 +448,6 @@ export type UninstallPluginResponse = {
success: boolean
}
export type PluginsFromMarketplaceResponse = {
plugins: Plugin[]
bundles?: Plugin[]
total: number
}
export type PluginsFromMarketplaceByInfoResponse = {
list: {
plugin: Plugin
version: {
plugin_name: string
plugin_org: string
unique_identifier: string
}
}[]
}
export type GitHubItemAndMarketPlaceDependency = {
type: 'github' | 'marketplace' | 'package'
value: {

View File

@ -1,5 +1,5 @@
import type { SearchParamsFromCollection } from '@dify/contracts/marketplace'
import type { useMarketplace } from '../hooks'
import type { SearchParamsFromCollection } from '@/app/components/plugins/marketplace/types'
import type { Plugin } from '@/app/components/plugins/types'
import type { Collection } from '@/app/components/tools/types'
import { render, screen } from '@testing-library/react'

View File

@ -1,6 +1,6 @@
import type { SearchParamsFromCollection } from '@dify/contracts/marketplace'
import type { ToolsContentInset } from '../content-inset'
import type { useMarketplace } from './hooks'
import type { SearchParamsFromCollection } from '@/app/components/plugins/marketplace/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
RiArrowRightUpLine,

View File

@ -1,3 +0,0 @@
import { oc } from '@orpc/contract'
export const base = oc.$route({ inputStructure: 'detailed' })

View File

@ -1,94 +0,0 @@
import type { InferContractRouterInputs } from '@orpc/contract'
import type { CollectionsAndPluginsSearchParams, MarketplaceCollection, PluginsSearchParams } from '@/app/components/plugins/marketplace/types'
import type { Plugin, PluginsFromMarketplaceResponse } from '@/app/components/plugins/types'
import type { MarketplaceTemplate } from '@/types/marketplace-template'
import { type } from '@orpc/contract'
import { base } from './base'
const collectionsContract = base
.route({
path: '/collections',
method: 'GET',
})
.input(
type<{
query?: CollectionsAndPluginsSearchParams & { page?: number, page_size?: number }
}>(),
)
.output(
type<{
data?: {
collections?: MarketplaceCollection[]
}
}>(),
)
const collectionPluginsContract = base
.route({
path: '/collections/{collectionId}/plugins',
method: 'POST',
})
.input(
type<{
params: {
collectionId: string
}
body?: CollectionsAndPluginsSearchParams
}>(),
)
.output(
type<{
data?: {
plugins?: Plugin[]
}
}>(),
)
const searchAdvancedContract = base
.route({
path: '/{kind}/search/advanced',
method: 'POST',
})
.input(type<{
params: {
kind: 'plugins' | 'bundles'
}
body: Omit<PluginsSearchParams, 'type'>
}>())
.output(type<{ data: PluginsFromMarketplaceResponse }>())
const templateDetailContract = base
.route({
path: '/templates/{templateId}',
method: 'GET',
})
.input(type<{
params: {
templateId: string
}
}>())
.output(type<{ data: MarketplaceTemplate }>())
const downloadPluginContract = base
.route({
path: '/plugins/{organization}/{pluginName}/{version}/download',
method: 'GET',
})
.input(type<{
params: {
organization: string
pluginName: string
version: string
}
}>())
.output(type<Blob>())
export const marketplaceRouterContract = {
collections: collectionsContract,
collectionPlugins: collectionPluginsContract,
searchAdvanced: searchAdvancedContract,
templateDetail: templateDetailContract,
downloadPlugin: downloadPluginContract,
}
export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRouterContract>

View File

@ -12,6 +12,7 @@ import type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'
import type { JsonifiedClient } from '@orpc/openapi-client'
import type { RouterUtils, TanstackQueryOperationContext } from '@orpc/tanstack-query'
import type { InfiniteData, QueryClient, QueryKey } from '@tanstack/react-query'
import { marketplaceRouterContract } from '@dify/contracts/marketplace'
import { createORPCClient, onError } from '@orpc/client'
import { OpenAPILink } from '@orpc/openapi-client/fetch'
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
@ -21,7 +22,6 @@ import {
IS_MARKETPLACE,
MARKETPLACE_API_PREFIX,
} from '@/config'
import { marketplaceRouterContract } from '@/contract/marketplace'
import { isClient } from '@/utils/client'
// eslint-disable-next-line no-restricted-imports
import { request } from './base'

View File

@ -1,7 +1,7 @@
import type { PluginInfoFromMarketPlace } from '@dify/contracts/marketplace'
import type {
Dependency,
InstallPackageResponse,
PluginInfoFromMarketPlace,
PluginManifestInMarket,
TaskStatusResponse,
UninstallPluginResponse,

View File

@ -1,4 +1,9 @@
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,
@ -21,9 +26,6 @@ import type {
Plugin,
PluginDeclaration,
PluginDetail,
PluginInfoFromMarketPlace,
PluginsFromMarketplaceByInfoResponse,
PluginsFromMarketplaceResponse,
PluginTask,
PluginTaskStart,
ReferenceSetting,

View File

@ -1,11 +0,0 @@
export type MarketplaceTemplate = {
id: string
template_name: string
overview: string
icon: string
icon_background: string
icon_file_key: string
publisher_unique_handle: string
usage_count: number
categories: string[]
}